Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/ENGINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1552,7 +1552,7 @@ bundled with the installer), never a 7-Zip installed on the PC. It runs as a sep

| Format | Read | Write |
|---|---|---|
| ZIP, JAR, WBPZ | 7-Zip | ZIP in-process (System.IO.Compression): UTF-8 names with the language-encoding flag, times, empty folders |
| ZIP, JAR, WBPZ, ZXP | 7-Zip | ZIP in-process (System.IO.Compression): UTF-8 names with the language-encoding flag, times, empty folders |
| Minecraft MCPACK, MCADDON, MCWORLD, MCTEMPLATE; Modrinth MRPACK | 7-Zip (they are ZIPs) | ZIP in-process, with the marker file at the top (see below) |
| 7Z | 7-Zip | 7-Zip |
| TAR | 7-Zip | in-process (System.Formats.Tar, PAX: UTF-8 names, times) |
Expand All @@ -1579,6 +1579,10 @@ bundled with the installer), never a 7-Zip installed on the PC. It runs as a sep
an archive or a folder whose only top folder holds the marker gets that folder's content, not the folder
(`GamePacks.Root`); an add-on (MCADDON, several packs side by side) is packed as it is. The presets (MCPACK …
MRPACK) are in the palette only.
- **Adobe extension packages** (`.zxp`, #1382, read only like JAR): the panels and plug-ins of After Effects, Premiere
Pro, Photoshop and the other Creative Cloud apps are signed ZIPs (`mimetype`, the extension's files and
`META-INF/signatures.xml`). They extract and convert to every archive target with the signature file inside.
Nothing writes `.zxp`: Adobe's apps install only packages signed with the developer's certificate (ZXPSignCmd).
- **Compress into one archive** (`Archive.CombineIntoOne`, the "One ZIP" preset): `JobQueue` hands all dropped files,
of any type and unconverted, to `IFileCombiner` (implemented by this engine). The archive is named after the first
file with the output pattern; files with the same name become `name (2).ext`. ZIP, TAR and TAR.GZ need no 7-Zip.
Expand Down
4 changes: 2 additions & 2 deletions src/Filee.Core/Formats/FormatFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
"fods", "fodp", "rst", "tex", "org", "adoc", "typ", "mediawiki", "dbk", "ipynb", "textile", "man",
"jsonl", "yaml", "toml", "har", "plist", "htmlz", "txtz", "chm", "dv", "mxf", "cavs", "usm", "m4s", "dav", "bik",
"h265", "h264", "ac3", "caf", "mp2", "dss", "wv", "tta", "w64", "eac3", "ape", "tak", "dsf", "dff",
"dts", "tar", "tgz", "tbz2", "txz", "tar7z", "tlz", "gz", "bz2", "xz", "lz", "lzma", "jar", "mcpack", "mcaddon",
"dts", "tar", "tgz", "tbz2", "txz", "tar7z", "tlz", "gz", "bz2", "xz", "lz", "lzma", "jar", "zxp", "mcpack", "mcaddon",
"mcworld", "mctemplate", "mrpack", "cab",
"cpio", "deb", "rpm", "dwg", "ttf", "otf", "woff", "woff2", "ass", "ssa", "ttml", "scc", "glb", "gltf",
"stl", "obj", "3mf", "ply", "fbx", "geojson", "kml", "kmz", "gpx", "shp", "fit", "osm", "xm", "it"
Expand Down Expand Up @@ -107,7 +107,7 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
([
"ppm", "icns", "qoi", "cur", "xpm", "xbm", "pam", "pfm", "miff", "fodg", "fodt", "fods", "fodp", "man",
"jsonl", "yaml", "toml", "har", "plist", "m4s", "tar", "tgz", "tbz2", "txz", "tar7z", "tlz", "gz", "bz2", "xz",
"lz", "lzma", "jar", "cab", "cpio", "deb", "rpm"
"lz", "lzma", "jar", "zxp", "cab", "cpio", "deb", "rpm"
], ["developer"]),
// Markup sources, papers and outlines, maintenance-doc drawings (CGM), Calibre's ebook formats.
(["cgm", "rst", "tex", "org", "adoc", "typ", "mediawiki", "dbk", "textile", "htmlz", "txtz"], ["writing"]),
Expand Down
3 changes: 3 additions & 0 deletions src/Filee.Core/Formats/FormatRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ public static class FormatRegistry
new("lzma", "LZMA", FormatCategory.Archive, ["lzma"], Writable: false),
new("z", "Z", FormatCategory.Archive, ["z"], Writable: false),
new("jar", "JAR", FormatCategory.Archive, ["jar"], Writable: false),
// Adobe extension packages for After Effects, Premiere Pro, Photoshop … (#1382): signed ZIPs. Never a target,
// since installing one needs the signature its developer makes with Adobe's ZXPSignCmd.
new("zxp", "ZXP (Adobe extension)", FormatCategory.Archive, ["zxp"], Writable: false),
// Minecraft Bedrock packs, add-ons, worlds and world templates, and Modrinth modpacks (#824, #898): ZIP archives
// under their own extensions, written with their marker file (manifest.json, level.dat, modrinth.index.json) at
// the top.
Expand Down
2 changes: 1 addition & 1 deletion src/Filee.Core/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public static bool IsFlipKeyAllowed(string key, IEnumerable<TriggerGesture> gest
public sealed class AppSettings
{
/// <summary>Current schema version. Increase when the format changes and add a migration.</summary>
public const int CurrentSchemaVersion = 81;
public const int CurrentSchemaVersion = 82;

public int SchemaVersion { get; set; } = CurrentSchemaVersion;

Expand Down
4 changes: 4 additions & 0 deletions src/Filee.Core/Settings/UserDataStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,10 @@ public static bool ApplyToLibrary(int from, List<Preset> presets, List<ToolbarPr
// renumbered if another change takes it first.)
if (from < 81)
changed |= LearnExtensions(profiles, out _, ["xm", "it", "s3m"]);
// v82: Adobe extension packages (.zxp, #1382) learn the archives donut; custom claims stay put. (Schema 82 may be
// renumbered if another change takes it first.)
if (from < 82)
changed |= LearnExtensions(profiles, out _, ["zxp"]);
return changed;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Filee.Engines/Archives/ArchiveExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal sealed class ArchiveExtractor(SevenZip? sevenZip, Func<string, long>? s
/// <summary>Formats read by 7-Zip.</summary>
public static readonly IReadOnlyList<string> SevenZipFormats =
[
"zip", "jar", "7z", "rar", "tar", "tgz", "tbz2", "txz", "tar7z", "tz", "gz", "bz2", "xz", "lzma", "z",
"zip", "jar", "zxp", "7z", "rar", "tar", "tgz", "tbz2", "txz", "tar7z", "tz", "gz", "bz2", "xz", "lzma", "z",
"cab", "cpio", "deb", "rpm", "dmg", "iso", "img", "lha", "arj", "split",
"mcpack", "mcaddon", "mcworld", "mctemplate", "mrpack",
];
Expand Down
8 changes: 4 additions & 4 deletions tests/Filee.Core.Tests/FormatFieldsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private static string[] Hidden(IReadOnlySet<string> turnedOff) =>
"psw", "qoi", "ras", "raw", "rb", "rm", "rpm", "rst", "s3m", "scc", "sda", "sdc", "sdw", "sgi", "shp", "snb", "spx",
"ssa", "stl", "sub", "swf", "tak", "tar", "tar7z", "tbz2", "tcr", "tex", "textile", "tgz", "tlz", "toml",
"tta", "ttf", "ttml", "txtz", "txz", "typ", "tz", "usm", "vob", "voc", "w64", "wbmp", "wmf", "woff", "woff2",
"wpd", "wtv", "wv", "xbm", "xcf", "xlt", "xm", "xpm", "xz", "yaml", "z"
"wpd", "wtv", "wv", "xbm", "xcf", "xlt", "xm", "xpm", "xz", "yaml", "z", "zxp"
];

public static TheoryData<string, string[]> HiddenWithOneFieldOff => new()
Expand All @@ -51,7 +51,7 @@ private static string[] Hidden(IReadOnlySet<string> turnedOff) =>
{ "developer", [
"bz2", "cab", "cpio", "cur", "deb", "fodg", "fodp", "fods", "fodt", "gz", "har", "icns", "jar",
"jsonl", "lz", "lzma", "m4s", "man", "miff", "pam", "pfm", "plist", "ppm", "qoi", "rpm", "tar", "tar7z",
"tbz2", "tgz", "tlz", "toml", "txz", "xbm", "xpm", "xz", "yaml"
"tbz2", "tgz", "tlz", "toml", "txz", "xbm", "xpm", "xz", "yaml", "zxp"
] },
{ "writing", ["adoc", "cgm", "dbk", "htmlz", "mediawiki", "org", "rst", "tex", "textile", "txtz", "typ"] },
{ "video-pro", [
Expand All @@ -78,7 +78,7 @@ private static string[] Hidden(IReadOnlySet<string> turnedOff) =>
{ "developer", [
"bz2", "cab", "caf", "chm", "cpio", "cur", "deb", "fodg", "fodp", "fods", "fodt", "gz", "har",
"icns", "ipynb", "jar", "jsonl", "lz", "lzma", "m4s", "man", "miff", "pam", "pfm", "plist", "ppm", "qoi",
"rpm", "tar", "tar7z", "tbz2", "tgz", "tlz", "toml", "txz", "xbm", "xpm", "xz", "yaml"
"rpm", "tar", "tar7z", "tbz2", "tgz", "tlz", "toml", "txz", "xbm", "xpm", "xz", "yaml", "zxp"
] },
{ "writing", ["adoc", "cgm", "chm", "dbk", "htmlz", "mediawiki", "org", "rst", "tex", "textile", "txtz", "typ"] },
{ "video-pro", [
Expand Down Expand Up @@ -145,7 +145,7 @@ public void Grade_counts_match_the_published_table()
{
[FormatGrade.Everyday] = 19,
[FormatGrade.General] = 97,
[FormatGrade.Specialist] = 117,
[FormatGrade.Specialist] = 118,
[FormatGrade.Legacy] = 52,
},
counts);
Expand Down
76 changes: 76 additions & 0 deletions tests/Filee.Core.Tests/ZxpFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Adobe extension packages (.zxp, #1382): a read-only archive on the archives donut, learned by existing libraries.

using Filee.Core.Formats;
using Filee.Core.Presets;
using Filee.Core.Profiles;
using Filee.Core.Settings;

namespace Filee.Core.Tests;

public class ZxpFormatTests
{
[Fact]
public void Zxp_is_an_archive_filee_reads_but_never_writes()
{
var zxp = FormatRegistry.Detect("Panel.ZXP");
Assert.NotNull(zxp);
Assert.Equal(("zxp", FormatCategory.Archive, false), (zxp.Id, zxp.Category, zxp.Writable));
Assert.Equal("archives", ProfileSelector.Select(BuiltInData.CreateProfiles(), ["Panel.zxp"])?.Id);
}

[Fact]
public void Version_81_libraries_learn_zxp_once()
{
var presets = BuiltInData.CreatePresets();
var profiles = BuiltInData.CreateProfiles();
var archives = profiles.Single(p => p.Id == "archives");
archives.Extensions.Remove("zxp");

Assert.True(SettingsMigrations.ApplyToLibrary(81, presets, profiles));

Assert.Contains("zxp", archives.Extensions);
Assert.False(SettingsMigrations.ApplyToLibrary(81, presets, profiles));
}

[Fact]
public void A_version_81_library_leaves_zxp_on_the_users_own_donut()
{
var presets = BuiltInData.CreatePresets();
var profiles = BuiltInData.CreateProfiles();
var archives = profiles.Single(p => p.Id == "archives");
archives.Extensions.Remove("zxp");
profiles.Insert(0, new ToolbarProfile { Id = "my-extensions", Name = "Extensions", Extensions = ["ZXP"] });

Assert.False(SettingsMigrations.ApplyToLibrary(81, presets, profiles));

Assert.DoesNotContain("zxp", archives.Extensions, StringComparer.OrdinalIgnoreCase);
Assert.Equal("my-extensions", ProfileSelector.Select(profiles, ["panel.zxp"])?.Id);
}

[Fact]
public void A_saved_version_81_library_learns_zxp_on_the_next_start_only()
{
using var dir = new TempDir();
var store = new UserDataStore(dir.Path);
store.Load();
store.Settings.SchemaVersion = 81;
foreach (var profile in store.Profiles)
profile.Extensions.Remove("zxp");
store.SaveLibrary();
store.SaveSettings();

var upgraded = new UserDataStore(dir.Path);
upgraded.Load();
var archives = upgraded.Profiles.Single(p => p.Id == "archives").Extensions;
Assert.Contains("zxp", archives);
Assert.Equal(AppSettings.CurrentSchemaVersion, upgraded.Settings.SchemaVersion);

// The user takes it off the archives donut again: the start after that leaves it off.
archives.Remove("zxp");
upgraded.SaveLibrary();
upgraded.SaveSettings();
var reloaded = new UserDataStore(dir.Path);
reloaded.Load();
Assert.DoesNotContain("zxp", reloaded.Profiles.Single(p => p.Id == "archives").Extensions);
}
}
3 changes: 2 additions & 1 deletion tests/Filee.Engines.Tests/ArchiveIsoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public void Every_supported_archive_has_a_direct_iso_route(string format)
[InlineData("alz")]
[InlineData("egg")]
[InlineData("jar")]
[InlineData("zxp")]
public async Task Archives_become_iso_with_nested_unicode_files_and_empty_directories(string format)
{
Assert.SkipUnless(SevenZip.Locate() is not null, "7-Zip is required.");
Expand All @@ -76,7 +77,7 @@ public async Task Archives_become_iso_with_nested_unicode_files_and_empty_direct
Directory.CreateDirectory(Path.Combine(tree, "nested"));
Directory.CreateDirectory(Path.Combine(tree, "empty"));
File.WriteAllText(Path.Combine(tree, "nested", "ν•œκΈ€.txt"), "contents");
await new ArchiveConverter().CombineAsync(Directory.GetFileSystemEntries(tree), input, format == "jar" ? "zip" : format,
await new ArchiveConverter().CombineAsync(Directory.GetFileSystemEntries(tree), input, format is "jar" or "zxp" ? "zip" : format,
new Preset(), _root, null, TestContext.Current.CancellationToken);
}
var original = File.ReadAllBytes(input);
Expand Down
43 changes: 43 additions & 0 deletions tests/Filee.Engines.Tests/ArchiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private async Task<ConversionJob> ConvertOkAsync(string source, Preset preset)
[Theory]
[InlineData("zip")]
[InlineData("jar")]
[InlineData("zxp")]
[InlineData("7z")]
[InlineData("rar")]
[InlineData("tgz")]
Expand Down Expand Up @@ -829,4 +830,46 @@ public async Task A_wbpz_that_is_no_archive_fails()
Assert.Equal(JobState.Failed, job.State);
Assert.Empty(job.Outputs);
}

// ───────────────────────── Adobe extension packages (issue #1382) ─────────────────────────

[Fact]
public void A_zxp_is_read_but_never_written()
{
var planner = fx.Catalog.CreatePlanner(["archive"]);
Assert.Null(planner.Plan("zip", "zxp"));
Assert.Null(planner.Plan("7z", "zxp"));
}

[Fact]
public async Task An_adobe_zxp_extracts_and_converts_to_zip_with_its_signature_file()
{
SkipWithout7Zip();
var input = Path.Combine(fx.NewFolder(), "panel.zxp");
// The layout ZXPSignCmd writes: the mimetype first, the extension's files, the signature in META-INF.
var expected = new Dictionary<string, byte[]>
{
["mimetype"] = Encoding.ASCII.GetBytes("application/vnd.adobe.air-ucf-package+zip"),
["CSXS/manifest.xml"] = Encoding.UTF8.GetBytes("<ExtensionManifest ExtensionBundleId=\"com.example.panel\"/>"),
["client/index.html"] = Encoding.UTF8.GetBytes("<!doctype html><title>νŒ¨λ„</title>"),
["META-INF/signatures.xml"] = Encoding.UTF8.GetBytes("<signatures/>"),
};
using (var archive = ZipFile.Open(input, ZipArchiveMode.Create))
foreach (var (name, bytes) in expected)
{
using var entry = archive.CreateEntry(name, name == "mimetype" ? CompressionLevel.NoCompression : CompressionLevel.Optimal).Open();
entry.Write(bytes);
}
var original = await File.ReadAllBytesAsync(input, Ct);

var folder = (await ConvertOkAsync(input, Extract)).Outputs.Single();
foreach (var (name, bytes) in expected)
Assert.Equal(bytes, await File.ReadAllBytesAsync(Path.Combine(folder, name), Ct));

var zip = (await ConvertOkAsync(input, new Preset { TargetFormat = "zip" })).Outputs.Single();
Assert.EndsWith("panel.zip", zip);
using var output = ZipFile.OpenRead(zip);
Assert.Equal(expected.Keys.Order(), output.Entries.Where(e => !e.FullName.EndsWith('/')).Select(e => e.FullName).Order());
Assert.Equal(original, await File.ReadAllBytesAsync(input, Ct));
}
}
Loading