Skip to content
Closed
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
8 changes: 4 additions & 4 deletions src/Filee.Core/Formats/FormatFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
]),
(FormatGrade.General,
[
"tiff", "bmp", "ico", "avif", "jxl", "tga", "apng", "jxr", "livp", "exe", "svg", "svgz", "emf", "vsd", "odg",
"tiff", "bmp", "ico", "avif", "jxl", "tga", "apng", "jxr", "livp", "svg", "svgz", "emf", "vsd", "odg",
"docm", "dotx", "dotm", "doc", "odt", "ott", "rtf", "wps", "ofd", "abw", "pages", "pub", "eml", "msg",
"p7m", "xlsm", "xltx", "xls", "ods", "ots", "tsv", "et", "numbers", "pptm", "potx", "potm", "ppsx", "ppt",
"odp", "otp", "key", "dps", "md", "html", "mhtml", "opml", "json", "xml", "vcf", "ics", "epub", "kepub",
"azw3", "fb2", "cbz", "cbr", "cb7", "cbt", "cbc", "m4v", "mkv", "webm", "avi", "wmv", "mpg", "ts",
"m2ts", "ogv", "m4b", "aac", "flac", "ogg", "opus", "wma", "aiff", "weba", "mka", "qta", "7z", "rar",
"dmg", "iso", "img", "alz", "egg", "split", "dxf", "srt", "vtt", "smi", "lrc"
"m2ts", "ogv", "m4b", "m4r", "aac", "flac", "ogg", "opus", "wma", "aiff", "weba", "mka", "qta", "7z", "rar",
"dmg", "iso", "img", "udf", "alz", "egg", "split", "dxf", "srt", "vtt", "smi", "lrc"
]),
(FormatGrade.Specialist,
[
Expand All @@ -82,7 +82,7 @@ .. Enum.GetValues<FormatCategory>().Select(category => category.ToString().ToLow
"dts", "tar", "tgz", "tbz2", "txz", "tar7z", "tlz", "gz", "bz2", "xz", "lz", "lzma", "jar", "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"
"stl", "obj", "3mf", "ply", "fbx", "geojson", "kml", "kmz", "gpx", "shp", "fit"
]),
(FormatGrade.Legacy,
[
Expand Down
30 changes: 11 additions & 19 deletions src/Filee.Core/Formats/FormatRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ public static class FormatRegistry
/// <summary>Pseudo format for archive extraction outputs and folders explicitly classified at input handling.</summary>
public const string Folder = "folder";

/// <summary>
/// Windows programs and libraries (.exe, .dll), read as the icon Explorer shows for them (#1188). They are never run.
/// </summary>
public const string Program = "exe";

/// <summary>All known formats. Order is the order shown in format pickers.</summary>
public static IReadOnlyList<FileFormat> Known { get; } =
[
Expand Down Expand Up @@ -65,8 +60,6 @@ public static class FormatRegistry
new("kra", "Krita (KRA)", FormatCategory.Image, ["kra", "kra~"], Writable: false),
// iPhone Live Photo as one ZIP (iCloud downloads): the photo, or its video with FFmpeg.
new("livp", "Live Photo (LIVP)", FormatCategory.Image, ["livp"], Writable: false),
// A Windows program or library (#1188), read as its icon: the picture Explorer shows for the file.
new(Program, "Windows program icon (EXE, DLL)", FormatCategory.Image, ["exe", "dll"], Writable: false),
new("raw", "RAW", FormatCategory.Image,
["dng", "cr2", "cr3", "crw", "nef", "nrw", "arw", "srf", "sr2", "orf", "rw2", "raf", "pef", "dcr", "kdc",
"erf", "mos", "mrw", "x3f", "3fr", "fff", "iiq", "mef", "rwl", "srw", "raw"], Writable: false),
Expand Down Expand Up @@ -155,12 +148,15 @@ public static class FormatRegistry
new("sda", "SDA", FormatCategory.Presentation, ["sda", "sdd"], Writable: false),

// Plain text / markup
// Text logs, source code, database schemas (DBML), proxy scripts, style sheets and settings files are read as plain
// text, never executed, compiled or translated (issues #273, #284, #254, #388, #368, #607, #536, #630, #661, #660,
// #864, #828, #980, #997, #854, #1176).
// Text logs, source code, database schemas (DBML), diagrams written as text (Mermaid), proxy scripts, style sheets
// and settings files are read as plain text, never executed, compiled or translated (issues #273, #284, #254, #388,
// #368, #607, #536, #630, #661, #660, #864, #828, #980, #997, #854, #1176, #1309). Rust, Go, Kotlin, Swift, Scala,
// SCSS, Vue and TSX sources join the other code files; ".js", ".php" and shell scripts stay out (programs and web
// pages, see WebFileNames).
new("txt", "TXT", FormatCategory.Text,
["txt", "log", "py", "pac", "bat", "cmd", "command", "lua", "css", "ps1", "sql", "ahk", "ini", "rsc",
"c", "h", "cpp", "cc", "cxx", "hpp", "hh", "hxx", "cs", "dart", "hql", "dbml", "java"]),
"c", "h", "cpp", "cc", "cxx", "hpp", "hh", "hxx", "cs", "dart", "hql", "dbml", "java",
"mermaid", "mmd", "rs", "go", "kt", "swift", "scala", "scss", "vue", "tsx"]),
new("md", "Markdown", FormatCategory.Text, ["md", "markdown"]),
new("html", "HTML", FormatCategory.Text, ["html", "htm", "xhtml"]),
// A web page saved with its pictures in one MIME file (Chrome, Edge, Word); read as the page it holds.
Expand Down Expand Up @@ -266,6 +262,8 @@ public static class FormatRegistry
new("mp3", "MP3", FormatCategory.Audio, ["mp3"]),
new("m4a", "M4A", FormatCategory.Audio, ["m4a"]),
new("m4b", "M4B", FormatCategory.Audio, ["m4b"]),
// iPhone ringtones (#1291): AAC in the same MP4 container as M4A, under the extension iTunes and iOS look for.
new("m4r", "M4R", FormatCategory.Audio, ["m4r"]),
new("aac", "AAC", FormatCategory.Audio, ["aac"]),
new("wav", "WAV", FormatCategory.Audio, ["wav", "wave"]),
new("flac", "FLAC", FormatCategory.Audio, ["flac"]),
Expand Down Expand Up @@ -330,6 +328,8 @@ public static class FormatRegistry
new("dmg", "DMG", FormatCategory.Archive, ["dmg"], Writable: false),
new("iso", "ISO", FormatCategory.Archive, ["iso"]),
new("img", "IMG", FormatCategory.Archive, ["img"], Writable: false),
// UDF disc images (#1338): DVD, Blu-ray and data discs, unpacked by 7-Zip like ISO files. Never a target.
new("udf", "UDF", FormatCategory.Archive, ["udf"], Writable: false),
new("lha", "LHA", FormatCategory.Archive, ["lha", "lzh"], Writable: false),
new("arj", "ARJ", FormatCategory.Archive, ["arj"], Writable: false),
new("alz", "ALZ", FormatCategory.Archive, ["alz"], Writable: false),
Expand Down Expand Up @@ -377,8 +377,6 @@ public static class FormatRegistry
new("shp", "Shapefile", FormatCategory.Geo, ["shp"]),
// Activity files read by the geo engine (issue #451); .fits/.fts stay FITS.
new("fit", "Garmin FIT", FormatCategory.Geo, ["fit"], Writable: false),
// OpenStreetMap XML exports read by the geo engine (issue #713); the binary .osm.pbf is not read.
new("osm", "OpenStreetMap", FormatCategory.Geo, ["osm"], Writable: false),
];

private static readonly Dictionary<string, FileFormat> ById =
Expand Down Expand Up @@ -406,12 +404,6 @@ public static FileFormat Get(string id) =>
/// <summary>Detects the format of a file from its extension, or <c>null</c> if it is not supported.</summary>
public static FileFormat? Detect(string path) => FindByExtension(ExtensionOf(path));

/// <summary>
/// True for a Windows program or library (<see cref="Program"/>). Filee reads its icon when the file is dropped or
/// named, but never opens one (it would start the program) and never takes one from a folder by itself.
/// </summary>
public static bool IsProgram(string path) => Detect(path)?.Id == Program;

/// <summary>
/// The extension of <paramref name="path"/> without the dot, including known two-part extensions
/// ("archive.tar.gz" → "tar.gz", "photo.jpg" → "jpg").
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 @@ -132,7 +132,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 = 78;
public const int CurrentSchemaVersion = 77;

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

Expand Down
180 changes: 180 additions & 0 deletions src/Filee.Core/Settings/ArchiveExtractor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Unpacks every archive format Filee reads into a folder: the bundled 7-Zip for most of them, Filee's own readers for
// ALZ, EGG and lzip. Compressed TARs (.tar.gz, .tar.lz …) and RPM payloads are unpacked stage by stage until the
// files appear. Every stage is checked first (encryption, paths leaving the folder, disk space) and links are removed.
// A single compressed stream (.gz, .bz2 …) is written by Filee as 7-Zip decodes it, so it can't fill the drive. The
// archive's Mark of the Web goes onto the unpacked files.

using Filee.Core.Formats;
using Filee.Core.Platform;

namespace Filee.Engines.Archives;

/// <summary>Unpacks archives into scratch folders.</summary>
/// <param name="streamLimit">
/// The most a single compressed stream may write into a folder; <see cref="ArchiveSafety.StreamLimit"/> unless a test
/// sets a small one.
/// </param>
internal sealed class ArchiveExtractor(SevenZip? sevenZip, Func<string, long>? streamLimit = null)

Check failure on line 17 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / metadata (linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 17 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04-arm, linux-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 17 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04, linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 17 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 17 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)
{
/// <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",
"cab", "cpio", "deb", "rpm", "dmg", "iso", "img", "udf", "lha", "arj", "split",
"mcpack", "mcaddon", "mcworld", "mctemplate", "mrpack",
];

/// <summary>Formats read by Filee's own code (lzip still needs 7-Zip's LZMA decoder).</summary>
public static readonly IReadOnlyList<string> BuiltInFormats = ["alz", "egg", "lz", "tlz"];

/// <summary>Everything that can be unpacked.</summary>
public static IEnumerable<string> ReadableFormats => SevenZipFormats.Concat(BuiltInFormats);

/// <summary>A TAR inside a compressed stream (or a 7z): unpacking the outer layer yields one .tar file.</summary>
private static readonly HashSet<string> TarWrapped = ["tgz", "tbz2", "txz", "tar7z", "tz", "tlz"];

/// <summary>
/// One compressed stream, no names or sizes to check beforehand (bzip2 states no size, gzip one modulo 4 GiB that
/// its author chose).
/// </summary>
private static readonly HashSet<string> SingleStream = ["gz", "bz2", "xz", "lzma", "z", "zst", "tgz", "tbz2", "txz", "tz"];

/// <summary>Formats an RPM payload comes in (7-Zip shows it as one compressed CPIO file).</summary>
private static readonly HashSet<string> RpmPayload = ["cpio", "gz", "bz2", "xz", "lzma", "z", "zst"];

private const int MaxStages = 3;

/// <summary>
/// Unpacks <paramref name="archive"/> (of format <paramref name="format"/>) into a new folder below
/// <paramref name="scratchRoot"/> and returns that folder.
/// </summary>
public async Task<string> ExtractAsync(string archive, string format, string scratchRoot, CancellationToken cancellationToken)
{
var current = archive;
var currentFormat = format;
string? previous = null;
for (var stage = 0; ; stage++)
{
var folder = Path.Combine(scratchRoot, "x" + stage);
Directory.CreateDirectory(folder);
await ExtractOnceAsync(current, currentFormat, folder, scratchRoot, cancellationToken);
ArchiveSafety.RemoveLinks(folder);
if (previous is not null)
ArchiveFiles.TryDeleteFolder(previous);

if (stage + 1 < MaxStages && InnerArchive(format, folder) is { } inner)
{
previous = folder;
(current, currentFormat) = inner;
continue;
}
return folder;
}
}

private async Task ExtractOnceAsync(string archive, string format, string folder, string scratchRoot, CancellationToken cancellationToken)
{
var name = Path.GetFileName(archive);
switch (format)
{
case "alz":
await Task.Run(() => AlzReader.Extract(archive, folder, cancellationToken), cancellationToken);
PassOnMark(archive, folder);
break;
case "egg":
await EggReader.ExtractAsync(archive, folder, sevenZip, scratchRoot, cancellationToken);
PassOnMark(archive, folder);
break;
case "lz":
case "tlz":
var output = FormatRegistry.NameWithoutExtension(archive) + (format == "tlz" ? ".tar" : "");
await LzipReader.DecompressAsync(archive, Path.Combine(folder, output), Require(), scratchRoot, cancellationToken);
PassOnMark(archive, folder);
break;
default:
var tool = Require();
var entries = await tool.ListAsync(archive, cancellationToken);
ArchiveSafety.CheckListing(entries, name, folder);
// 7-Zip lists no name for some (.bz2, .xz), and never more than one file.
if (SingleStream.Contains(format) && entries.Count(e => !e.IsFolder) <= 1)
{
await DecodeStreamAsync(tool, archive, format, entries.FirstOrDefault(e => !e.IsFolder), folder, cancellationToken);
PassOnMark(archive, folder);
break;
}
var links = entries.Where(e => e.IsSymbolicLink).Select(e => e.Path).ToList();
await tool.ExtractAsync(archive, folder, links, scratchRoot, cancellationToken);
break;
}
}

/// <summary>
/// Unpacks a single compressed stream into <paramref name="folder"/> under the name 7-Zip gives it (the one stored
/// in a .gz, else the archive's name without its extension), stopping at <see cref="ArchiveSafety.StreamLimit"/>.
/// </summary>
private async Task DecodeStreamAsync(SevenZip tool, string archive, string format, SevenZipEntry? entry,

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / metadata (linux-x64)

The type or namespace name 'SevenZipEntry' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / metadata (linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04-arm, linux-arm64)

The type or namespace name 'SevenZipEntry' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04-arm, linux-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04, linux-x64)

The type or namespace name 'SevenZipEntry' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04, linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type or namespace name 'SevenZipEntry' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'SevenZipEntry' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 115 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)
string folder, CancellationToken cancellationToken)
{
var name = Path.GetFileName(archive);
var entryName = entry is { Path.Length: > 0 }
? entry.Path
: FormatRegistry.NameWithoutExtension(archive) + (TarWrapped.Contains(format) ? ".tar" : "");
var path = ArchiveSafety.ResolveEntryPath(folder, entryName, name);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
var limit = (streamLimit ?? ArchiveSafety.StreamLimit)(folder);
var tooLarge = ArchiveSafety.TooLarge(name, limit, folder); // with the free space before writing
try
{
await using var output = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.None, 1 << 16);
await tool.DecodeAsync(archive, output, limit, () => tooLarge, cancellationToken);
}
catch
{
ArchiveFiles.TryDeleteFile(path);
throw;
}
File.SetLastWriteTimeUtc(path, entry?.Modified ?? File.GetLastWriteTimeUtc(archive));
}

/// <summary>
/// Gives the files Filee's own code unpacked into <paramref name="folder"/> the archive's Mark of the Web, if it has
/// one (7-Zip does this itself for the files it writes).
/// </summary>
private static void PassOnMark(string archive, string folder) =>
MarkOfTheWeb.Copy(archive, Directory.EnumerateFiles(folder, "*",
new EnumerationOptions { RecurseSubdirectories = true, AttributesToSkip = FileAttributes.ReparsePoint }));

/// <summary>The single archive file a stage produced that must be unpacked as well, or null when done.</summary>
private static (string Path, string Format)? InnerArchive(string originalFormat, string folder)
{
if (!TarWrapped.Contains(originalFormat) && originalFormat != "rpm")
return null;
var entries = Directory.GetFileSystemEntries(folder);
if (entries.Length != 1 || !File.Exists(entries[0]))
return null;

var file = entries[0];
if (IsTar(file))
return (file, "tar");
if (originalFormat == "rpm")
{
var detected = FormatRegistry.Detect(file)?.Id ?? Path.GetExtension(file).TrimStart('.').ToLowerInvariant();
if (RpmPayload.Contains(detected))
return (file, detected);
}
return null;
}

/// <summary>True for a .tar file or a file with the "ustar" signature at offset 257.</summary>
internal static bool IsTar(string path)
{
if (path.EndsWith(".tar", StringComparison.OrdinalIgnoreCase))
return true;
Span<byte> header = stackalloc byte[262];
using var stream = File.OpenRead(path);
return stream.ReadAtLeast(header, header.Length, throwOnEndOfStream: false) == header.Length
&& header[257..262].SequenceEqual("ustar"u8);
}

private SevenZip Require() => sevenZip ?? throw new InvalidOperationException("7-Zip was not found (engines/7zip).");

Check failure on line 179 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / metadata (linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 179 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04-arm, linux-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 179 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / portable / portable (ubuntu-24.04, linux-x64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 179 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)

Check failure on line 179 in src/Filee.Core/Settings/ArchiveExtractor.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'SevenZip' could not be found (are you missing a using directive or an assembly reference?)
}
Loading
Loading