diff --git a/Config/products/macpacker.json b/Config/products/macpacker.json index f7e661ab..774e8bf5 100644 --- a/Config/products/macpacker.json +++ b/Config/products/macpacker.json @@ -35,6 +35,70 @@ "tr": "Bütünlük kontrolü ve kurtarma" }, "versions": [ + { + "version": "1.2.0", + "items": [ + { + "type": "feat", + "highlight": true, + "title": { + "en": "Open and extract LZMA files", + "zh-Hans": "打开并解压 LZMA 文件", + "fr": "Ouvrir et extraire les fichiers LZMA", + "de": "LZMA-Dateien öffnen und entpacken", + "it": "Aprire ed estrarre i file LZMA", + "ja": "LZMA ファイルを開いて展開", + "fa": "باز کردن و استخراج فایل‌های LZMA", + "pl": "Otwieranie i wypakowywanie plików LZMA", + "pt-BR": "Abrir e extrair arquivos LZMA", + "ru": "Открытие и извлечение файлов LZMA", + "uk": "Відкриття та розпакування файлів LZMA", + "es-MX": "Abrir y extraer archivos LZMA", + "ko": "LZMA 파일 열기 및 압축 해제", + "nl": "LZMA-bestanden openen en uitpakken", + "tr": "LZMA dosyalarını açma ve çıkarma", + "vi": "Mở và giải nén tệp LZMA", + "ar": "فتح ملفات LZMA واستخراجها", + "ca": "Obre i extreu fitxers LZMA", + "el": "Άνοιγμα και εξαγωγή αρχείων LZMA", + "nb": "Åpne og pakk ut LZMA-filer", + "zh-TW": "開啟並解壓縮 LZMA 檔案" + }, + "issues": [ + "340" + ] + }, + { + "type": "fix", + "title": { + "en": "Deb files open as one “unknown” file", + "zh-Hans": "deb 文件打开后只有一个“unknown”文件", + "fr": "Les fichiers deb s’ouvrent en un seul fichier « unknown »", + "de": "Deb-Dateien öffnen sich als eine Datei „unknown“", + "it": "I file deb si aprono come un unico file «unknown»", + "ja": "deb ファイルを開くと「unknown」だけが表示される", + "fa": "فایل‌های deb فقط به‌صورت یک فایل «unknown» باز می‌شوند", + "pl": "Pliki deb otwierają się jako jeden plik „unknown”", + "pt-BR": "Arquivos deb abrem como um único arquivo “unknown”", + "ru": "Deb-файлы открываются как один файл «unknown»", + "uk": "Deb-файли відкриваються як один файл «unknown»", + "es-MX": "Los archivos deb se abren como un solo archivo «unknown»", + "ko": "deb 파일이 “unknown” 파일 하나로 열림", + "nl": "Deb-bestanden openen als één bestand “unknown”", + "tr": "Deb dosyaları tek bir “unknown” dosyası olarak açılıyor", + "vi": "Tệp deb mở ra chỉ với một tệp “unknown”", + "ar": "ملفات deb تُفتح كملف واحد باسم «unknown»", + "ca": "Els fitxers deb s’obren com un sol fitxer «unknown»", + "el": "Τα αρχεία deb ανοίγουν ως ένα μόνο αρχείο «unknown»", + "nb": "Deb-filer åpnes som én fil kalt «unknown»", + "zh-TW": "deb 檔案開啟後只有一個「unknown」檔案" + }, + "issues": [ + "340" + ] + } + ] + }, { "version": "1.1.0", "items": [ diff --git a/MacPacker/Info.plist b/MacPacker/Info.plist index dc325bd9..95a63597 100644 --- a/MacPacker/Info.plist +++ b/MacPacker/Info.plist @@ -205,6 +205,43 @@ LSTypeIsPackage + + CFBundleTypeExtensions + + lzma + + CFBundleTypeName + LZMA Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Default + LSItemContentTypes + + org.tukaani.lzma-archive + + LSTypeIsPackage + + + + CFBundleTypeExtensions + + tlzma + tar.lzma + + CFBundleTypeName + LZMA Tarball Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Default + LSItemContentTypes + + org.tukaani.tar-lzma-archive + + LSTypeIsPackage + + CFBundleTypeExtensions @@ -1060,6 +1097,46 @@ + + UTTypeConformsTo + + public.archive + public.data + + UTTypeDescription + LZMA File + UTTypeIcons + + UTTypeIdentifier + org.tukaani.lzma-archive + UTTypeTagSpecification + + public.filename-extension + + lzma + + + + + UTTypeConformsTo + + public.archive + public.data + + UTTypeDescription + LZMA Tar Archive + UTTypeIcons + + UTTypeIdentifier + org.tukaani.tar-lzma-archive + UTTypeTagSpecification + + public.filename-extension + + tlzma + + + ITSAppUsesNonExemptEncryption diff --git a/MacPacker/Info_Store.plist b/MacPacker/Info_Store.plist index e7c10304..2e708e6b 100644 --- a/MacPacker/Info_Store.plist +++ b/MacPacker/Info_Store.plist @@ -205,6 +205,43 @@ LSTypeIsPackage + + CFBundleTypeExtensions + + lzma + + CFBundleTypeName + LZMA Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Default + LSItemContentTypes + + org.tukaani.lzma-archive + + LSTypeIsPackage + + + + CFBundleTypeExtensions + + tlzma + tar.lzma + + CFBundleTypeName + LZMA Tarball Archive + CFBundleTypeRole + Viewer + LSHandlerRank + Default + LSItemContentTypes + + org.tukaani.tar-lzma-archive + + LSTypeIsPackage + + CFBundleTypeExtensions @@ -1054,6 +1091,46 @@ + + UTTypeConformsTo + + public.archive + public.data + + UTTypeDescription + LZMA File + UTTypeIcons + + UTTypeIdentifier + org.tukaani.lzma-archive + UTTypeTagSpecification + + public.filename-extension + + lzma + + + + + UTTypeConformsTo + + public.archive + public.data + + UTTypeDescription + LZMA Tar Archive + UTTypeIcons + + UTTypeIdentifier + org.tukaani.tar-lzma-archive + UTTypeTagSpecification + + public.filename-extension + + tlzma + + + ITSAppUsesNonExemptEncryption diff --git a/Modules/Sources/CSevenZip/sevenzip_bridge.cpp b/Modules/Sources/CSevenZip/sevenzip_bridge.cpp index 464b8367..7e324ff8 100644 --- a/Modules/Sources/CSevenZip/sevenzip_bridge.cpp +++ b/Modules/Sources/CSevenZip/sevenzip_bridge.cpp @@ -1125,6 +1125,24 @@ static void sz_indexAppleDoubleSidecars(SZArchiveHandle *handle) { } } +/// Open an archive at the given filesystem path. +/// +/// Tries each registered 7-Zip handler on the file, then drills into nested +/// containers (disk images containing filesystems) up to a fixed depth via +/// the `kpidMainSubfile` stream. A `deb` level is not drilled into: its +/// payload is a tar under a compression wrapper with no usable entry paths, +/// which would otherwise list as a lone entry named "unknown" instead of the +/// deb's members. +/// +/// @param path Filesystem path of the archive (UTF-8). +/// @param password Password for reading encrypted headers (7z `-mhe=on`, +/// RAR `-hp`), or NULL. Also pre-sets the extraction password. +/// @param needs_password_out If non-NULL, set to true when a handler asked +/// for a password during the open. Meaningful only on failure: it tells the +/// caller to prompt and retry rather than report an unsupported file. +/// @param error_out If non-NULL, set to a malloc'd UTF-8 error string on +/// failure; caller must free() it. +/// @return Opaque archive handle on success, NULL on failure. SZArchiveRef sz_open(const char *path, const char *password, bool *needs_password_out, char **error_out) { if (needs_password_out) *needs_password_out = false; @@ -1196,6 +1214,15 @@ SZArchiveRef sz_open(const char *path, const char *password, for (int depth = 0; depth < kMaxNestingDepth; depth++) { IInArchive *currentArc = handle->levels.back().archive; + // Do not drill into a deb: its payload is a tar under a compression + // wrapper whose level has no usable kpidPath, which would render a + // lone entry named "unknown" instead of the deb's members. + NWindows::NCOM::CPropVariant ownExt; + currentArc->GetArchiveProperty(kpidExtension, &ownExt); + if (ownExt.vt == VT_BSTR && ownExt.bstrVal + && wcscmp(ownExt.bstrVal, L"deb") == 0) + break; + // Check if this archive signals a nested container NWindows::NCOM::CPropVariant prop; currentArc->GetArchiveProperty(kpidMainSubfile, &prop); diff --git a/Modules/Sources/Core/Formats/Catalog.json b/Modules/Sources/Core/Formats/Catalog.json index 7a1b49fe..55c61e36 100644 --- a/Modules/Sources/Core/Formats/Catalog.json +++ b/Modules/Sources/Core/Formats/Catalog.json @@ -202,6 +202,18 @@ { "id": "swc", "capabilities": ["listContents", "extractFiles"], "default": true } ] }, + { + "id": "lzma", + "name": "LZMA File", + "kind": "compression", + "uti": ["org.tukaani.lzma-archive"], + "extensions": ["lzma"], + "mime": ["application/x-lzma"], + "rules": [], + "engines": [ + { "id": "7zip", "capabilities": ["listContents", "extractFiles"], "default": true } + ] + }, { "id": "lzx", "name": "LZX Archive", @@ -571,6 +583,13 @@ "extensions": ["tlz4","tar.lz4"], "components": ["tar","lz4"] }, + { + "id": "tar.lzma", + "name": "LZMA Tar Archive", + "uti": ["org.tukaani.tar-lzma-archive"], + "extensions": ["tlzma", "tar.lzma"], + "components": ["tar","lzma"] + }, { "id": "tar.xz", "name": "XZ Tar Archive", diff --git a/Modules/Tests/CoreTests/DefaultArchiveTests.swift b/Modules/Tests/CoreTests/DefaultArchiveTests.swift index e2dd99c0..9edf1a80 100644 --- a/Modules/Tests/CoreTests/DefaultArchiveTests.swift +++ b/Modules/Tests/CoreTests/DefaultArchiveTests.swift @@ -68,11 +68,13 @@ extension AllCoreTests { // compounds ("tar.bz2", "tar", 5, 2), ("tar.gz", "tar", 5, 2), + ("tar.lzma", "tar", 5, 2), ("tar.xz", "tar", 5, 2), ("tar.Z", "tar", 5, 2), // comounds with one ending ("tbz2", "tar", 5, 2), ("tgz", "tar", 5, 2), + ("tlzma", "tar", 5, 2), ("txz", "tar", 5, 2), ("taz", "tar", 5, 2) ]) @@ -97,6 +99,27 @@ extension AllCoreTests { #expect(state.root === state.selectedItem) } + /// A deb lists its members, not a lone "unknown". dpkg-deb names the + /// first member `debian-binary`, and 7-Zip then offers the compressed + /// `data.tar.*` as the main subfile, whose one nameless stream was all + /// that showed. fpm names it `debian-binary/`, which 7-Zip reads as a + /// plain ar, so that deb always listed its members. + @Test(arguments: [ + ("defaultarchive-dpkg.deb", ["control.tar.xz", "data.tar.xz"]), + ("defaultarchive.deb", ["control.tar.gz", "data.tar.gz", "debian-binary"]) + ]) + func debListsItsMembers(arg: (String, [String])) async throws { + let state = ArchiveState(catalog: ArchiveTypeCatalog(), engineSelector: ArchiveEngineSelector7zip()) + let url = Bundle.module.url(forResource: "defaultArchives", withExtension: nil)! + .appendingPathComponent(arg.0) + + state.open(url: url) + try await state.openTask?.value + + let names = state.root?.children?.compactMap { state.entries[$0]?.name }.sorted() + #expect(names == arg.1) + } + @Test("Test all defaultArchives on xad", arguments: [ ("zip", "zip"), ("7z", "7zip"), diff --git a/Modules/Tests/CoreTests/TestArchives b/Modules/Tests/CoreTests/TestArchives index a4f5a291..00abea7c 160000 --- a/Modules/Tests/CoreTests/TestArchives +++ b/Modules/Tests/CoreTests/TestArchives @@ -1 +1 @@ -Subproject commit a4f5a291276cb1257f3f918971d8a9c2d5c586c3 +Subproject commit 00abea7ccff3411420ed05ebe2bafd096e780160 diff --git a/QuickLookExtension/Info.plist b/QuickLookExtension/Info.plist index 50a7d6a7..5b4853e2 100644 --- a/QuickLookExtension/Info.plist +++ b/QuickLookExtension/Info.plist @@ -35,6 +35,7 @@ org.gnu.gnu-zip-archive org.gnu.gnu-zip-tar-archive org.tukaani.lzma-archive + org.tukaani.tar-lzma-archive org.tukaani.tar-xz-archive org.tukaani.xz-archive public.archive diff --git a/README.md b/README.md index 89e826ee..58ffea4b 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ MacPacker reads and extracts 40+ formats across archives, disk images, and compr | --- | --- | | **Archives** | 7z · aar · apk · ar · arj · cab · cbr · cbz · chm · cpio · deb · exe · jar · lha · lzh · lzx · msi · pkg · rar · rpm · sea · sit · sitx · tar · xar · zip · zipx | | **Disk images** | dmg · fat · iso · ntfs · qcow2 · squashfs · vdi · vhd · vhdx · vmdk · wim | -| **Compression** | bz2 · gz · lz4 · xz · z | -| **Tarballs** | tar.gz (tgz) · tar.bz2 (tbz2) · tar.xz (txz) · tar.lz4 · tar.z (taz) | +| **Compression** | bz2 · gz · lz4 · lzma · xz · z | +| **Tarballs** | tar.gz (tgz) · tar.bz2 (tbz2) · tar.lzma (tlzma) · tar.xz (txz) · tar.lz4 · tar.z (taz) | | **Split archives** | zip.001 · z01 · 7z.001 | See the [full format reference and per-format guides](https://macpacker.app/en/docs) on the website.