From 8ef7bcd50d3b7a4e3f598a7938b7112b13b6c7e5 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 07:14:35 +0200 Subject: [PATCH 01/16] + a Defragment ribbon tab gives the block map the whole window Maintenance lived in a separate 2200-line dialog that squeezed the block map between an image picker, a strategy group, an output box and two rows of buttons. It is now a contextual "Disk Tools" tab, Defragment, shown while the open or selected image supports any maintenance operation. Selecting it hides the browser (not torn down, so leaving restores it exactly) and gives the client area to the block map, files panel, colour key and a progress line. Ribbon groups: - Operation: Defragment, Optimize, Shrink, Compact, Clear, Purge, Scramble as a radio group; Start; Stop where cancellable. - Defrag Mode: Consolidate (+ Pack at End), Defrag (FillHolesLazy), Re-order (AscendingOrder), Sort Entries, Carve Hole with size/offset fields shown only while picked. - Options: interleave (1-256, refused outside rather than clamped), metadata zone, layout profile + Edit Profiles; seed, chunk placement and minimal geometry appear with their operation. - View: Blocks / Circle / 3D Stack, Files, Legend, Analyze. Every item asks one adapter, MaintenanceCapabilities, which is the only place that maps registry interfaces to answers; modes and layout options are probed against the descriptor's own refusal guard with a stream that throws on first touch, and only for descriptors that implement the options overload themselves (the interface default would accept and ignore them). Sort Entries is answered "no" for every format: no descriptor offers it through the registry yet. Optimize is offered only where an optimizer changes something (ZIP/gzip/zlib/MacBinary/compound tar re-encode, 7z solid regrouping, CVF clusters, file-internal chunks), not where ArchiveOperations.Optimize would copy the file through. The window's non-UI logic moved into MaintenancePresenter (choices -> DefragOptions, running each verb off the UI thread, progress) and BlockMapSnapshot (layout reading); a refusal (NotSupportedException) is reported and leaves the image untouched, a change re-lists the archive in place, and a nested archive is maintained as a temporary copy written back after each change (MaintenanceSession). DefragmentWindow is gone; Tools > Maintenance (Ctrl+Shift+D) and the list's Maintenance context entries open the tab with their operation picked, and the Archive tab keeps only Reconfigure. While browsing a folder, the archive left behind is no longer a maintenance target. The headless test backend gains a real combo-box peer: the interface takes a ReadOnlySpan, which a dispatch proxy cannot box. Verified: FAT, ZIP and partitioned-disk capabilities; interleave and hole parsing boundaries; strategy -> engine mode mapping; tab visibility, client-area swap and restore, per-capability enabling, carve fields, view toggles; Start on a scrambled FAT keeps size and every file byte-identical and reports no fragmentation afterwards; an oversized carve hole is refused with the image byte-identical. --- .../Maintenance/BlockMapSnapshot.cs | 389 +++ .../Maintenance/MaintenanceCapabilities.cs | 204 ++ .../Maintenance/MaintenanceInput.cs | 66 + .../Maintenance/MaintenancePresenter.cs | 475 ++++ .../Maintenance/MaintenanceVerb.cs | 71 + Compression.NativeUI/ScreenshotMode.cs | 10 +- .../ViewModels/MainViewModel.cs | 143 +- .../ViewModels/MaintenanceSession.cs | 32 + Compression.NativeUI/Views/DefragmentView.cs | 218 ++ .../Views/DefragmentWindow.cs | 2221 ----------------- .../Views/MainForm.Defragment.cs | 603 +++++ Compression.NativeUI/Views/MainForm.Ribbon.cs | 16 +- Compression.NativeUI/Views/MainForm.cs | 22 +- Compression.Registry/DefragRebuilder.cs | 6 +- Compression.Tests/NativeUI/HeadlessBackend.cs | 2 +- .../NativeUI/MainFormTests.Defragment.cs | 307 +++ Compression.Tests/NativeUI/MainFormTests.cs | 3 +- .../NativeUI/MaintenanceFixtures.cs | 80 + .../NativeUI/MaintenancePresenterTests.cs | 228 ++ Compression.Tests/NativeUI/RecordingPeers.cs | 23 + 20 files changed, 2793 insertions(+), 2326 deletions(-) create mode 100644 Compression.NativeUI/Maintenance/BlockMapSnapshot.cs create mode 100644 Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs create mode 100644 Compression.NativeUI/Maintenance/MaintenanceInput.cs create mode 100644 Compression.NativeUI/Maintenance/MaintenancePresenter.cs create mode 100644 Compression.NativeUI/Maintenance/MaintenanceVerb.cs create mode 100644 Compression.NativeUI/ViewModels/MaintenanceSession.cs create mode 100644 Compression.NativeUI/Views/DefragmentView.cs delete mode 100644 Compression.NativeUI/Views/DefragmentWindow.cs create mode 100644 Compression.NativeUI/Views/MainForm.Defragment.cs create mode 100644 Compression.Tests/NativeUI/MainFormTests.Defragment.cs create mode 100644 Compression.Tests/NativeUI/MaintenanceFixtures.cs create mode 100644 Compression.Tests/NativeUI/MaintenancePresenterTests.cs diff --git a/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs b/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs new file mode 100644 index 000000000..0a7cefd0f --- /dev/null +++ b/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs @@ -0,0 +1,389 @@ +using Compression.Registry; + +namespace Compression.NativeUI.Maintenance; + +/// +/// One row of the files panel: name, size, fragment count, method, modification date and thermal +/// class. The fragment count comes from whatever layout the format offered; formats that offer none +/// show an em dash rather than a figure nothing measured. +/// +internal sealed class FileRow { + public string Name { get; init; } = ""; + public long Size { get; init; } + public string SizeDisplay { get; init; } = ""; + public string FragmentsDisplay { get; init; } = ""; + public string MethodDisplay { get; init; } = ""; + public DateTime? Modified { get; init; } + public string ModifiedDisplay { get; init; } = ""; + public string Class { get; init; } = ""; +} + +/// +/// What the block view shows of an image at rest: its extents, its size, the files panel rows and a +/// line saying how honest the picture is. +/// +internal sealed record BlockMapSnapshot(IReadOnlyList? Map, long ImageSize, IReadOnlyList Rows, string Status) { + /// Cap on extents fed to the block map; adjacent same-kind regions merge beyond it. + internal const int MaxExtents = 50_000; + + public static BlockMapSnapshot Empty(string status) => new(null, 0, [], status); + + /// + /// Reads the layout of the image at . A descriptor that exposes a real + /// extent, archive or chunk layout gets the honest picture; anything else falls back to sizes laid + /// end to end from zero, which the status line labels as approximate. + /// + public static BlockMapSnapshot Read(string path, IArchiveFormatOperations? ops) { + if (ops is null) return Empty("No layout: the format is not recognised."); + + if (ops is IFilesystemExtentMap extentMap && Try(() => FromExtentMap(path, extentMap, ops)) is { } extents) return extents; + if (ops is IArchiveLayoutMap archiveLayout && Try(() => FromArchiveLayout(path, archiveLayout, ops)) is { } archive) return archive; + if (ops is IFileInternalLayoutMap fileLayout && Try(() => FromFileInternalLayout(path, fileLayout)) is { } chunks) return chunks; + return Try(() => Approximate(path, ops)) ?? Empty("No layout could be read."); + + // A walker failing mid-stream falls through to the next, so the user still sees something. + static BlockMapSnapshot? Try(Func read) { + try { + return read(); + } catch { + return null; + } + } + } + + private static BlockMapSnapshot? FromExtentMap(string path, IFilesystemExtentMap extentMap, IArchiveFormatOperations ops) { + using var stream = File.OpenRead(path); + var imageSize = stream.Length; + if (imageSize <= 0) return null; + + var filled = NormalizeExtents([.. extentMap.EnumerateExtents(stream)], imageSize); + if (filled is null) return null; + + Dictionary? mtimeByName = null; + List? entries = null; + try { + stream.Position = 0; + entries = ops.List(stream, password: null); + mtimeByName = entries.Where(e => !e.IsDirectory).GroupBy(e => e.Name).ToDictionary(g => g.Key, g => g.First().LastModified); + } catch { + // Classification is best-effort; the layout itself is what matters here. + } + + var thresholds = mtimeByName is null ? null : ComputeMtimeQuartiles(mtimeByName.Values); + var classified = new List(filled.Count); + foreach (var ex in filled) + classified.Add(ex.Kind == DefragBlockKind.Used && ex.FileName is { } owner && mtimeByName is not null && mtimeByName.TryGetValue(owner, out var mtime) + ? ex with { Classification = thresholds is null ? DefragBlockClass.Normal : Classify(mtime, thresholds, 0, 1) } + : ex); + + var runs = CountRunsByOwner(classified); + IReadOnlyList rows = entries is null ? [] : [.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { + Name = e.Name, + Size = e.OriginalSize, + SizeDisplay = MaintenanceInput.FormatSize(e.OriginalSize), + FragmentsDisplay = runs.TryGetValue(e.Name, out var count) && count > 0 ? count.ToString("N0") : "—", + Modified = e.LastModified, + ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "—", + Class = "—", + })]; + + var usedExtents = classified.Count(ex => ex.Kind == DefragBlockKind.Used); + var fragmentedFiles = entries is null ? 0 : runs.Values.Count(r => r > 1); + var status = fragmentedFiles > 0 + ? $"Real on-disk layout — {usedExtents:N0} extents, {fragmentedFiles:N0} fragmented file(s)" + : $"Real on-disk layout — {usedExtents:N0} extents (no fragmentation detected)"; + return new(Bin(classified), imageSize, rows, status); + } + + private static BlockMapSnapshot? FromArchiveLayout(string path, IArchiveLayoutMap archiveLayout, IArchiveFormatOperations ops) { + using var stream = File.OpenRead(path); + var imageSize = stream.Length; + if (imageSize <= 0) return null; + + var filled = NormalizeExtents([.. archiveLayout.EnumerateLayout(stream)], imageSize); + if (filled is null) return null; + + Dictionary? methodByName = null; + List? entries = null; + try { + stream.Position = 0; + entries = ops.List(stream, password: null); + methodByName = entries.Where(e => !e.IsDirectory).GroupBy(e => e.Name).ToDictionary(g => g.Key, g => g.First().Method ?? ""); + } catch { + // Classification is best-effort. + } + + var classified = new List(filled.Count); + foreach (var ex in filled) + classified.Add(ex.Kind == DefragBlockKind.Used && ex.FileName is { } owner && methodByName is not null && methodByName.TryGetValue(owner, out var method) + ? ex with { Classification = ClassifyByMethod(method) } + : ex); + + // Counted off the layout the container reported. An entry the map does not name is not one run + // — it is unmeasured. + var runs = CountRunsByOwner(classified); + IReadOnlyList rows = entries is null ? [] : [.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { + Name = e.Name, + Size = e.OriginalSize, + SizeDisplay = MaintenanceInput.FormatSize(e.OriginalSize), + FragmentsDisplay = runs.TryGetValue(e.Name, out var count) ? count.ToString("N0") : "—", + MethodDisplay = e.Method ?? "", + Modified = e.LastModified, + ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "", + Class = ClassifyByMethod(e.Method ?? "").ToString(), + })]; + + var regionCount = classified.Count(ex => ex.Kind == DefragBlockKind.Used); + var freeBytes = classified.Where(ex => ex.Kind == DefragBlockKind.Free).Sum(ex => ex.Length); + var status = freeBytes > 0 + ? $"Real archive layout — {regionCount:N0} regions, {MaintenanceInput.FormatSize(freeBytes)} wasted" + : $"Real archive layout — {regionCount:N0} regions (tightly packed)"; + return new(Bin(classified), imageSize, rows, status); + } + + private static BlockMapSnapshot? FromFileInternalLayout(string path, IFileInternalLayoutMap fileLayout) { + using var stream = File.OpenRead(path); + var imageSize = stream.Length; + if (imageSize <= 0) return null; + + var filled = NormalizeExtents([.. fileLayout.EnumerateChunks(stream)], imageSize); + if (filled is null) return null; + + // A row here is one stretch of the file, so the count belongs to the chunk's owner and comes from + // the same layout the map was drawn from. + var runs = CountRunsByOwner(filled, onlyKind: null); + IReadOnlyList rows = [.. filled.Select(ch => new FileRow { + Name = ch.FileName ?? ch.Kind.ToString(), + Size = ch.Length, + SizeDisplay = MaintenanceInput.FormatSize(ch.Length), + FragmentsDisplay = ch.FileName is { } owner && runs.TryGetValue(owner, out var count) ? count.ToString("N0") : "—", + MethodDisplay = ch.Kind.ToString(), + Class = ch.Classification?.ToString() ?? "—", + })]; + + return new(filled, imageSize, rows, $"File-internal layout — {filled.Count(c => c.Kind != DefragBlockKind.Free):N0} chunks"); + } + + private static BlockMapSnapshot Approximate(string path, IArchiveFormatOperations ops) { + using var stream = File.OpenRead(path); + var entries = ops.List(stream, password: null); + var imageSize = stream.Length; + var map = new List(); + var rows = new List(); + var offset = 0L; + + var files = entries.Where(e => !e.IsDirectory).ToList(); + var thresholds = ComputeMtimeQuartiles(files.Select(f => f.LastModified)); + for (var k = 0; k < files.Count; ++k) { + var e = files[k]; + var size = Math.Max(1, e.OriginalSize); + var cls = Classify(e.LastModified, thresholds, k, files.Count); + map.Add(new(offset, size, DefragBlockKind.Used, e.Name, cls)); + rows.Add(new() { + Name = e.Name, + Size = e.OriginalSize, + SizeDisplay = MaintenanceInput.FormatSize(e.OriginalSize), + // The format reported no layout, so these tiles are sizes laid end to end rather than where + // anything actually is. Counting runs off that would report one for everything. + FragmentsDisplay = "—", + Modified = e.LastModified, + ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "—", + Class = cls.ToString(), + }); + + offset += size; + if (offset >= imageSize) break; + } + + if (offset < imageSize) map.Add(new(offset, imageSize - offset, DefragBlockKind.Free)); + return new(map, imageSize, rows, "Approximate layout: the format reports no positions, so files are drawn end to end"); + } + + /// + /// Projects the archive's entries onto the image's byte span, weighted by size, so a staged rebuild + /// has something to animate against before any output exists. + /// + public static IReadOnlyList? StagedProjection(string path, IArchiveFormatOperations ops, long displaySize) { + try { + using var stream = File.OpenRead(path); + var entries = ops.List(stream, null).Where(e => !e.IsDirectory).ToArray(); + if (entries.Length == 0) return null; + + var weights = entries.Select(e => Math.Max(1L, e.OriginalSize)).ToArray(); + var totalWeight = Math.Max(1L, weights.Sum()); + var size = Math.Max(1L, displaySize); + var map = new List(entries.Length); + long cumulative = 0; + long offset = 0; + for (var i = 0; i < entries.Length; ++i) { + cumulative += weights[i]; + var end = i == entries.Length - 1 ? size : (long)((double)cumulative / totalWeight * size); + end = Math.Clamp(end, offset, size); + if (end > offset) map.Add(new(offset, end - offset, DefragBlockKind.Used, entries[i].Name, ClassifyByMethod(entries[i].Method ?? ""))); + offset = end; + } + + return map; + } catch { + // The existing picture stays usable when a projection cannot be built. + return null; + } + } + + // ── helpers ───────────────────────────────────────────────────────────────────────────────── + + private static IReadOnlyList Bin(List extents) + => extents.Count > MaxExtents ? BinExtents(extents, MaxExtents) : extents; + + /// + /// Splits the modification times into four equal-count buckets. The most recent quarter is hot, + /// the oldest is frozen. + /// + internal static (DateTime? Cold, DateTime? Normal, DateTime? Hot)? ComputeMtimeQuartiles(IEnumerable times) { + var sorted = times.Where(t => t.HasValue).Select(t => t!.Value).Order().ToList(); + if (sorted.Count == 0) return null; + + var n = sorted.Count; + return (sorted[Math.Min(n - 1, n / 4)], sorted[Math.Min(n - 1, n / 2)], sorted[Math.Min(n - 1, 3 * n / 4)]); + } + + internal static DefragBlockClass Classify(DateTime? modified, (DateTime? Cold, DateTime? Normal, DateTime? Hot)? thresholds, int index, int count) { + if (thresholds is not { } t) + // No dates anywhere — fall back to listing-order quartile so the chart still has structure. + return (index * 4 / Math.Max(1, count)) switch { + 0 => DefragBlockClass.Hot, + 1 => DefragBlockClass.Normal, + 2 => DefragBlockClass.Cold, + _ => DefragBlockClass.Frozen, + }; + + // An undated entry in a dated set stands out as frozen rather than being guessed at. + if (modified is not { } when) return DefragBlockClass.Frozen; + if (when < t.Cold) return DefragBlockClass.Frozen; + if (when < t.Normal) return DefragBlockClass.Cold; + return when < t.Hot ? DefragBlockClass.Normal : DefragBlockClass.Hot; + } + + /// Maps a compression method name to a thermal class for colour-coding archive entries. + internal static DefragBlockClass ClassifyByMethod(string method) { + var m = method.ToUpperInvariant(); + if (m.Contains("STORE") || m.Contains("COPY") || m is "NONE" or "") return DefragBlockClass.Frozen; + if (m.Contains("LZMA") || m.Contains("PPMD") || m.Contains("BZIP")) return DefragBlockClass.Hot; + if (m.Contains("ZSTD")) return DefragBlockClass.Cold; + return DefragBlockClass.Normal; + } + + /// + /// Sorts extents by offset, clips them to the image, fills the gaps as free space and trims + /// overlaps so a later extent at the same offset wins. Returns null when nothing usable remains. + /// + internal static List? NormalizeExtents(List raw, long imageSize) { + if (raw.Count == 0) return null; + raw.Sort(static (a, b) => a.Offset.CompareTo(b.Offset)); + + var clipped = new List(raw.Count); + foreach (var ex in raw) { + if (ex.Offset >= imageSize) continue; + var length = Math.Min(ex.Length, imageSize - ex.Offset); + if (length <= 0) continue; + clipped.Add(ex with { Length = length }); + } + if (clipped.Count == 0) return null; + + var filled = new List(clipped.Count * 2); + var cursor = 0L; + foreach (var ex in clipped) { + if (ex.Offset > cursor) + filled.Add(new(cursor, ex.Offset - cursor, DefragBlockKind.Free)); + else if (ex.Offset < cursor) { + if (ex.Offset + ex.Length <= cursor) continue; + var trimmed = ex with { Length = ex.Length - (cursor - ex.Offset), Offset = cursor }; + filled.Add(trimmed); + cursor = trimmed.Offset + trimmed.Length; + continue; + } + + filled.Add(ex); + cursor = ex.Offset + ex.Length; + } + + if (cursor < imageSize) filled.Add(new(cursor, imageSize - cursor, DefragBlockKind.Free)); + return filled; + } + + /// + /// Bins an oversized extent list down to by merging adjacent same-kind + /// extents, then — if still over budget — by equal-sized byte bins taking each bin's dominant kind. + /// + internal static List BinExtents(List input, int maxCount) { + if (input.Count <= maxCount) return input; + + var merged = new List(input.Count); + foreach (var ex in input) { + if (merged.Count > 0 && merged[^1] is var last && last.Kind == ex.Kind && last.Offset + last.Length == ex.Offset && last.FileName == ex.FileName) + merged[^1] = last with { Length = last.Length + ex.Length }; + else + merged.Add(ex); + } + if (merged.Count <= maxCount) return merged; + + var imageSize = merged[^1].Offset + merged[^1].Length; + var bytesPerBin = (double)imageSize / maxCount; + var binned = new List(maxCount); + var binStart = 0L; + var binIndex = 0; + var binKind = DefragBlockKind.Free; + var binBestLength = 0L; + string? binBestName = null; + + foreach (var ex in merged) { + while (ex.Offset + ex.Length > binStart + bytesPerBin && binIndex < maxCount - 1) { + if (ex.Length > binBestLength) { + binBestLength = ex.Length; + binKind = ex.Kind; + binBestName = ex.FileName; + } + + binned.Add(new(binStart, (long)bytesPerBin, binKind, binBestName)); + binStart += (long)bytesPerBin; + ++binIndex; + binKind = DefragBlockKind.Free; + binBestLength = 0; + binBestName = null; + } + + if (ex.Length > binBestLength) { + binBestLength = ex.Length; + binKind = ex.Kind; + binBestName = ex.FileName; + } + } + + binned.Add(new(binStart, imageSize - binStart, binKind, binBestName)); + return binned; + } + + /// + /// How many separate stretches of the container each owner's data occupies. Counting the entries a + /// map yields would measure the map rather than the layout: some merge an owner's consecutive + /// blocks and some emit one per block. Stretches that end exactly where the next begins are one run. + /// + internal static Dictionary CountRunsByOwner(IEnumerable extents, DefragBlockKind? onlyKind = DefragBlockKind.Used) { + var runs = new Dictionary(StringComparer.Ordinal); + var endOf = new Dictionary(StringComparer.Ordinal); + + foreach (var extent in extents) { + if (onlyKind is { } wanted && extent.Kind != wanted) continue; + if (extent.FileName is not { } owner) continue; + + if (!endOf.TryGetValue(owner, out var previousEnd) || previousEnd != extent.Offset) { + runs.TryGetValue(owner, out var count); + runs[owner] = count + 1; + } + + endOf[owner] = extent.Offset + extent.Length; + } + + return runs; + } +} diff --git a/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs b/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs new file mode 100644 index 000000000..d2854bfe6 --- /dev/null +++ b/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs @@ -0,0 +1,204 @@ +using System.Collections.Concurrent; +using Compression.Lib; +using Compression.Registry; +using Compression.Registry.Layout; + +namespace Compression.NativeUI.Maintenance; + +/// One answer to "can this target do that?": yes or no, and the sentence that says why. +internal readonly record struct Capability(bool Supported, string Reason) { + public static Capability Yes(string detail) => new(true, detail); + public static Capability No(string reason) => new(false, reason); +} + +/// +/// What one format offers the Defragment tab: which operations, which defragmentation strategies and +/// which layout options. The ribbon enables exactly what this says and nothing else, so an option is +/// never offered only to be ignored or refused. +/// +/// +/// This is the single place that turns the registry's capability interfaces into those answers. +/// When the registry grows a first-class capability model, this file is the one that changes. +/// +internal sealed record MaintenanceCapabilities( + string FormatId, + IReadOnlyDictionary Verbs, + IReadOnlyDictionary Strategies, + Capability PackAtStart, + Capability PackAtEnd, + Capability Interleave, + Capability MetadataZone, + Capability LayoutProfile, + Capability ChunkPlacement, + Capability MinimalGeometry, + OptimizeRoute OptimizeRoute, + bool StagedDefragment) { + + private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal); + + /// Whether any operation at all can run on the target. + public bool AnySupported => this.Verbs.Values.Any(v => v.Supported); + + public Capability Verb(MaintenanceVerb verb) => this.Verbs.TryGetValue(verb, out var c) ? c : Capability.No("Not offered."); + public Capability Strategy(DefragStrategy strategy) => this.Strategies.TryGetValue(strategy, out var c) ? c : Capability.No("Not offered."); + + /// + /// The capabilities of . Answers depend on the descriptor alone, so they + /// are worked out once per format and shared. + /// + public static MaintenanceCapabilities For(string formatId) { + ArgumentNullException.ThrowIfNull(formatId); + return Cache.GetOrAdd(formatId, Evaluate); + } + + private static MaintenanceCapabilities Evaluate(string formatId) { + var ops = FormatRegistry.GetArchiveOps(formatId); + var descriptor = FormatRegistry.GetById(formatId); + var name = descriptor?.DisplayName is { Length: > 0 } display ? display : formatId; + + var defragmentable = ops as IArchiveDefragmentable; + var modes = defragmentable is null ? [] : ProbeModes(defragmentable); + var ownOptionsPath = defragmentable is not null && DeclaresOwnOptionsPath(defragmentable); + + var route = OptimizeRouteOf(formatId, ops); + var verbs = new Dictionary { + [MaintenanceVerb.Defragment] = modes.Count > 0 + ? Capability.Yes($"Lays {name} out again; size and contents unchanged.") + : Capability.No($"{name} has no lossless defragmenter."), + [MaintenanceVerb.Optimize] = route switch { + OptimizeRoute.FileInternal => Capability.Yes("Reorders the chunks inside the file (e.g. fast-start)."), + OptimizeRoute.CompressedVolume => Capability.Yes("Re-encodes every cluster, keeping whichever of compressed or stored is smaller."), + OptimizeRoute.SolidBlocks => Capability.Yes("Tries five solid-block groupings and keeps the smallest; the original stays until the winner is committed."), + OptimizeRoute.Reencode => Capability.Yes("Re-encodes with the strongest settings into a staged copy, then replaces the original."), + _ => Capability.No(defragmentable is not null + ? $"{name} has no optimizer; Defragment is its layout operation." + : $"{name} has no optimizer."), + }, + [MaintenanceVerb.Shrink] = formatId is "Fat" or "Ext" or "Ext1" or "Vhd" || ops is IArchiveShrinkable + ? Capability.Yes("Minimises the stored footprint, keeping every file.") + : Capability.No($"{name} cannot shrink."), + [MaintenanceVerb.Purge] = ops is IArchiveModifiable + ? Capability.Yes("Erases every file, leaving a valid empty container. Asks first.") + : Capability.No($"{name} cannot remove entries, so it cannot be purged."), + [MaintenanceVerb.WipeEmpty] = ops is IWipeEmpty + ? Capability.Yes("Zeroes unused space — free clusters, slack, dead bytes — and nothing else.") + : Capability.No($"{name} exposes no layout to tell free space from live data."), + [MaintenanceVerb.Compact] = ops is IArchiveDefragmentable or IArchiveShrinkable or IArchiveCreatable + ? Capability.Yes("Defragment, optimize and shrink in one pass.") + : Capability.No($"{name} offers none of the steps Compact is made of."), + [MaintenanceVerb.Scramble] = ops is IFilesystemScrambleable + ? Capability.Yes("Scatters every block on purpose, so Defragment has something to undo. Contents preserved; asks first.") + : Capability.No($"{name} cannot scatter a volume in place."), + }; + + var start = modes.Contains(DefragMode.ConsolidateAtStart); + var end = modes.Contains(DefragMode.ConsolidateAtEnd); + var strategies = new Dictionary { + [DefragStrategy.Consolidate] = start || end + ? Capability.Yes("Packs every file at one end; free space ends up in one piece.") + : Capability.No($"{name} cannot pack its files together."), + [DefragStrategy.Defrag] = modes.Contains(DefragMode.FillHolesLazy) + ? Capability.Yes("Fills existing holes from the tail, moving as few bytes as possible.") + : Capability.No($"{name} cannot fill holes lazily."), + [DefragStrategy.Reorder] = modes.Contains(DefragMode.AscendingOrder) + ? Capability.Yes("Moves only what reads backwards, so every file's blocks ascend.") + : Capability.No($"{name} cannot reorder blocks into ascending order."), + // No descriptor offers a directory-entry sort through the registry yet, so the honest answer + // for every format is no. It is still a capability answer: the day a descriptor gains it, this + // is the line that turns it on. + [DefragStrategy.SortEntries] = Capability.No($"{name} does not offer sorting directory entries."), + [DefragStrategy.CarveHole] = modes.Contains(DefragMode.CarveHole) + ? Capability.Yes("Reserves one contiguous free region of the size you choose.") + : Capability.No($"{name} cannot carve a hole."), + }; + + var probeMode = modes.Count > 0 ? modes[0] : DefragMode.ConsolidateAtStart; + Capability Option(string what, DefragOptions options) { + if (defragmentable is null) return Capability.No($"{name} has no defragmenter."); + if (!ownOptionsPath) return Capability.No($"{name} defragments by rebuilding, which ignores {what}."); + return Accepts(defragmentable, options) ? Capability.Yes($"{name} honours {what}.") : Capability.No($"{name} refuses {what}."); + } + + return new( + formatId, + verbs, + strategies, + PackAtStart: start ? Capability.Yes("Packs at the front; free space ends up after the last file.") : Capability.No($"{name} cannot pack at the start."), + PackAtEnd: end ? Capability.Yes("Packs at the back instead; free space stays at the front.") : Capability.No($"{name} cannot pack at the end."), + Interleave: Option("block interleave", new() { Mode = probeMode, InterleaveStride = 2 }), + MetadataZone: Option("metadata placement", new() { Mode = probeMode, MetadataZonePlacement = Registry.MetadataZone.Front }), + LayoutProfile: Option("layout profiles", new() { Mode = probeMode, LayoutTemplate = new LayoutTemplate { Name = "probe" } }), + ChunkPlacement: ops is IFileInternalChunkMover + ? Capability.Yes("Where metadata chunks go relative to the payload.") + : Capability.No($"{name} has no chunk placement to choose."), + MinimalGeometry: ops is IArchiveCreatable and IFormatOptionsSchema + ? Capability.Yes("Rebuilds at the smallest geometry the format allows; the result may not be a standard image.") + : Capability.No($"{name} has no geometry to minimise."), + route, + StagedDefragment: defragmentable is not null && !DeclaresOwnOptionsPath(defragmentable) && !DeclaresOwnPlainPath(defragmentable)); + } + + private static OptimizeRoute OptimizeRouteOf(string formatId, IArchiveFormatOperations? ops) { + if (ops is IFileInternalChunkMover) return OptimizeRoute.FileInternal; + if (formatId is "DoubleSpace" or "DriveSpace" or "DriveSpace3") return OptimizeRoute.CompressedVolume; + if (formatId == "SevenZip") return OptimizeRoute.SolidBlocks; + + // ArchiveOperations.Optimize re-encodes these; anything else it copies through unchanged, which + // is not an operation worth offering. + if (formatId is "Zip" or "Gzip" or "Zlib" or "MacBinary") return OptimizeRoute.Reencode; + return Enum.TryParse(formatId, out var format) && FormatDetector.GetTarCompression(format).HasValue + ? OptimizeRoute.Reencode + : OptimizeRoute.None; + } + + // ── probing ───────────────────────────────────────────────────────────────────────────────── + + /// The modes the descriptor accepts, in order. + private static List ProbeModes(IArchiveDefragmentable defragmentable) + => [.. Enum.GetValues().Where(mode => Accepts(defragmentable, new() { Mode = mode }))]; + + /// + /// Asks the descriptor whether it takes , without touching any image. + /// There is no way to ask directly, so it is handed a stream that throws on first touch: reaching + /// the I/O means the request was accepted, while before any I/O + /// means it was refused — the guard every lossless defragmenter runs first. + /// + private static bool Accepts(IArchiveDefragmentable defragmentable, DefragOptions options) { + try { + using var probe = new ProbeStream(); + defragmentable.Defragment(probe, options); + return true; // Unlikely: the probe should have been touched first. + } catch (ProbeAbortException) { + return true; + } catch { + // NotSupportedException is the refusal; anything else is not taken as a yes either. + return false; + } + } + + /// + /// Whether the descriptor implements the options overload itself. The interface default only + /// knows and rebuilds, so it would accept layout options + /// without applying them; only a descriptor's own implementation can be asked about those. + /// + private static bool DeclaresOwnOptionsPath(IArchiveDefragmentable defragmentable) + => defragmentable.GetType().GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream), typeof(DefragOptions)]) is not null; + + private static bool DeclaresOwnPlainPath(IArchiveDefragmentable defragmentable) + => defragmentable.GetType().GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream)]) is not null; + + private sealed class ProbeAbortException : Exception; + + private sealed class ProbeStream : Stream { + public override bool CanRead => true; + public override bool CanSeek => true; + public override bool CanWrite => true; + public override long Length => 1024; + public override long Position { get => 0; set => throw new ProbeAbortException(); } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); + public override long Seek(long offset, SeekOrigin origin) => throw new ProbeAbortException(); + public override void SetLength(long value) => throw new ProbeAbortException(); + public override void Write(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); + } +} diff --git a/Compression.NativeUI/Maintenance/MaintenanceInput.cs b/Compression.NativeUI/Maintenance/MaintenanceInput.cs new file mode 100644 index 000000000..25ec1e4e4 --- /dev/null +++ b/Compression.NativeUI/Maintenance/MaintenanceInput.cs @@ -0,0 +1,66 @@ +using System.Globalization; + +namespace Compression.NativeUI.Maintenance; + +/// A value typed into the ribbon, read: either the value, or the reason it cannot be used. +internal readonly record struct Parsed(T Value, string? Error) { + public bool IsValid => this.Error is null; + public static Parsed Ok(T value) => new(value, null); + public static Parsed Fail(string error) => new(default!, error); +} + +/// Reads and formats the few values the Defragment tab takes as text. +internal static class MaintenanceInput { + public const int MinInterleave = 1; + public const int MaxInterleave = 256; + + /// + /// The block interleave stride: 1 is contiguous, 2 every other block, up to 256. Anything outside + /// that is refused rather than clamped, so what runs is what was typed. + /// + public static Parsed ParseInterleave(string? text) { + var trimmed = text?.Trim() ?? ""; + if (!int.TryParse(trimmed, NumberStyles.None, CultureInfo.InvariantCulture, out var stride)) + return Parsed.Fail($"Interleave must be a whole number from {MinInterleave} to {MaxInterleave}."); + return stride is < MinInterleave or > MaxInterleave + ? Parsed.Fail($"Interleave {stride} is outside {MinInterleave}–{MaxInterleave}.") + : Parsed.Ok(stride); + } + + /// A byte count with an optional k/m/g suffix (KiB, MiB, GiB): 64m, 1g, 524288. + public static Parsed ParseSize(string? text) { + var s = text?.Trim().ToLowerInvariant() ?? ""; + var multiplier = 1L; + if (s.EndsWith('k')) (multiplier, s) = (1L << 10, s[..^1]); + else if (s.EndsWith('m')) (multiplier, s) = (1L << 20, s[..^1]); + else if (s.EndsWith('g')) (multiplier, s) = (1L << 30, s[..^1]); + + if (!long.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var n) || n <= 0) + return Parsed.Fail("Hole size must be a positive size such as 64m, 1g or 524288."); + return n > long.MaxValue / multiplier + ? Parsed.Fail("Hole size is too large.") + : Parsed.Ok(n * multiplier); + } + + /// Where a carved hole starts: auto (after the last live extent, -1) or a byte offset. + public static Parsed ParseHoleAt(string? text) { + var s = text?.Trim() ?? ""; + if (s.Length == 0 || s.Equals("auto", StringComparison.OrdinalIgnoreCase)) return Parsed.Ok(-1); + return long.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var offset) + ? Parsed.Ok(offset) + : Parsed.Fail("Hole offset must be 'auto' or a byte offset."); + } + + /// Scramble's seed: any 32-bit integer. The same seed deals the same layout. + public static Parsed ParseSeed(string? text) + => int.TryParse(text?.Trim(), NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var seed) + ? Parsed.Ok(seed) + : Parsed.Fail("Seed must be a whole number."); + + public static string FormatSize(long bytes) => bytes switch { + < 1024 => $"{bytes} B", + < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", + < 1024 * 1024 * 1024 => $"{bytes / (1024.0 * 1024):F1} MB", + _ => $"{bytes / (1024.0 * 1024 * 1024):F2} GB", + }; +} diff --git a/Compression.NativeUI/Maintenance/MaintenancePresenter.cs b/Compression.NativeUI/Maintenance/MaintenancePresenter.cs new file mode 100644 index 000000000..2dd1febfc --- /dev/null +++ b/Compression.NativeUI/Maintenance/MaintenancePresenter.cs @@ -0,0 +1,475 @@ +using System.Diagnostics; +using Compression.Lib; +using Compression.Registry; +using Compression.Registry.Layout; + +namespace Compression.NativeUI.Maintenance; + +/// A live update from a running operation, for the block view. +internal sealed record MaintenanceProgress( + IReadOnlyList? Map, + long ImageSize, + long ReadHead, + long WriteHead, + double Fraction, + string? Status, + bool CommitStarted = false); + +internal enum MaintenanceOutcomeKind { Succeeded, Refused, Failed, Cancelled } + +/// How an operation ended, in one line, and whether the image on disk changed. +internal sealed record MaintenanceOutcome(MaintenanceOutcomeKind Kind, string Summary, bool Mutated); + +/// +/// The Defragment tab without the tab: one target image, what its format supports, what the user +/// picked, and the code that runs the picked operation off the UI thread. The view binds to this and +/// marshals the callbacks; nothing here touches a control. +/// +internal sealed class MaintenancePresenter { + /// The operations in the order the ribbon offers them; the first supported one is preselected. + public static readonly MaintenanceVerb[] VerbOrder = [ + MaintenanceVerb.Defragment, MaintenanceVerb.Optimize, MaintenanceVerb.Shrink, MaintenanceVerb.Compact, + MaintenanceVerb.WipeEmpty, MaintenanceVerb.Purge, MaintenanceVerb.Scramble, + ]; + + public MaintenancePresenter(string imagePath, string formatId) { + this.ImagePath = imagePath; + this.FormatId = formatId; + this.Capabilities = MaintenanceCapabilities.For(formatId); + this.Verb = VerbOrder.Cast().FirstOrDefault(v => this.Capabilities.Verb(v!.Value).Supported); + this.Strategy = Enum.GetValues().FirstOrDefault(s => this.Capabilities.Strategy(s).Supported); + } + + public string ImagePath { get; } + public string FormatId { get; } + public MaintenanceCapabilities Capabilities { get; } + public IArchiveFormatOperations? Operations => FormatRegistry.GetArchiveOps(this.FormatId); + + // ── what the user picked ──────────────────────────────────────────────────────────────────── + + public MaintenanceVerb? Verb { get; set; } + public DefragStrategy Strategy { get; set; } + public bool PackAtEnd { get; set; } + public string InterleaveText { get; set; } = "1"; + public MetadataZone MetadataZone { get; set; } = MetadataZone.Unchanged; + public LayoutTemplate? LayoutProfile { get; set; } + public string HoleSizeText { get; set; } = "64m"; + public string HoleAtText { get; set; } = "auto"; + public string SeedText { get; set; } = "1"; + public bool MinimalGeometry { get; set; } + public MetadataPlacementProfile? ChunkPlacement { get; set; } + + public bool IsRunning { get; private set; } + + /// + /// Why Start cannot run as things stand, or null when it can: no operation, one the format does + /// not support, a strategy it does not support, or a value that does not read. + /// + public string? StartBlocker { + get { + if (this.IsRunning) return "An operation is already running."; + if (this.Verb is not { } verb) return "Pick an operation."; + if (this.Capabilities.Verb(verb) is { Supported: false } refused) return refused.Reason; + if (verb == MaintenanceVerb.Defragment && this.Capabilities.Strategy(this.Strategy) is { Supported: false } strategy) return strategy.Reason; + return this.Validate().FirstOrDefault(); + } + } + + /// Every value that would have to change before the picked operation could run. + public IReadOnlyList Validate() { + var errors = new List(); + switch (this.Verb) { + case MaintenanceVerb.Defragment: + if (this.Capabilities.Interleave.Supported && MaintenanceInput.ParseInterleave(this.InterleaveText).Error is { } stride) errors.Add(stride); + if (this.Strategy == DefragStrategy.CarveHole) { + if (MaintenanceInput.ParseSize(this.HoleSizeText).Error is { } size) errors.Add(size); + if (MaintenanceInput.ParseHoleAt(this.HoleAtText).Error is { } at) errors.Add(at); + } + + break; + case MaintenanceVerb.Scramble: + if (MaintenanceInput.ParseSeed(this.SeedText).Error is { } seed) errors.Add(seed); + break; + } + + return errors; + } + + /// Whether the picked operation can be stopped part-way. + public bool CanCancel => this.Verb switch { + MaintenanceVerb.Defragment => true, + MaintenanceVerb.Optimize => this.Capabilities.OptimizeRoute is OptimizeRoute.Reencode or OptimizeRoute.SolidBlocks, + _ => false, + }; + + /// + /// Whether stopping discards a staged copy (the original untouched) rather than leaving in-place + /// moves where they got to. + /// + public bool IsStaged => this.Verb switch { + MaintenanceVerb.Defragment => this.Capabilities.StagedDefragment, + MaintenanceVerb.Optimize => true, + _ => false, + }; + + /// The engine mode the picked strategy stands for. + public DefragMode EngineMode => this.Strategy switch { + DefragStrategy.Defrag => DefragMode.FillHolesLazy, + DefragStrategy.Reorder => DefragMode.AscendingOrder, + DefragStrategy.CarveHole => DefragMode.CarveHole, + // Pack at the end when asked to, or when that is the only packing the format knows. + _ => this.PackAtEnd && this.Capabilities.PackAtEnd.Supported || !this.Capabilities.PackAtStart.Supported + ? DefragMode.ConsolidateAtEnd + : DefragMode.ConsolidateAtStart, + }; + + /// + /// The options a defragmentation runs with. Only what the format supports is set: an option the + /// ribbon disabled stays at its default rather than reaching the engine to be refused. + /// + public DefragOptions BuildDefragOptions(Action? onProgress = null, CancellationToken token = default) { + var mode = this.EngineMode; + var carve = mode == DefragMode.CarveHole; + return new() { + Mode = mode, + HoleSize = carve ? MaintenanceInput.ParseSize(this.HoleSizeText).Value : 0, + HoleAt = carve ? MaintenanceInput.ParseHoleAt(this.HoleAtText).Value : -1, + InterleaveStride = this.Capabilities.Interleave.Supported && MaintenanceInput.ParseInterleave(this.InterleaveText) is { IsValid: true } stride ? stride.Value : 1, + MetadataZonePlacement = this.Capabilities.MetadataZone.Supported ? this.MetadataZone : MetadataZone.Unchanged, + LayoutTemplate = this.Capabilities.LayoutProfile.Supported ? this.LayoutProfile : null, + OnProgress = onProgress, + CancellationToken = token, + }; + } + + /// + /// The question to ask before running, for the operations that destroy or deliberately degrade + /// something; null for those that only improve the image or leave it as it was. + /// + public string? ConfirmationFor() { + var file = Path.GetFileName(this.ImagePath); + switch (this.Verb) { + case MaintenanceVerb.Purge: + var count = 0; + try { + using var stream = File.OpenRead(this.ImagePath); + count = this.Operations?.List(stream, password: null).Count(e => !e.IsDirectory) ?? 0; + } catch { + // The count is for the question only; the operation itself reports any failure. + } + + return $"Erase ALL {count} file(s) from {file}?\n\nThis leaves a valid but empty container. The data cannot be recovered."; + case MaintenanceVerb.Scramble: + return $"Fragment {file} on purpose?\n\nEvery file's blocks are scattered across the volume. The contents are preserved exactly " + + "-- only the layout changes, and Defragment undoes it -- but this is a testing tool, not a repair."; + case MaintenanceVerb.Compact when this.MinimalGeometry && this.Capabilities.MinimalGeometry.Supported: + return "Minimal geometry rebuilds the container at the smallest size the format allows (e.g. a 1.44 MB FAT floppy collapses to a few KB).\n\n" + + "Contents are preserved, but the result may no longer be a standard, mountable image. Continue?"; + default: + return null; + } + } + + /// Reads the image's layout for the block view. + public BlockMapSnapshot Analyze() => BlockMapSnapshot.Read(this.ImagePath, this.Operations); + + // ── running ───────────────────────────────────────────────────────────────────────────────── + + /// + /// Runs the picked operation on a worker thread. and + /// are called from that thread; the caller marshals them. A format refusing + /// the request () ends as + /// with the image as it was. + /// + public Task RunAsync(Action progress, Action log, CancellationToken token = default) { + if (this.StartBlocker is { } blocker) return Task.FromResult(new MaintenanceOutcome(MaintenanceOutcomeKind.Refused, blocker, Mutated: false)); + + var verb = this.Verb!.Value; + var path = this.ImagePath; + this.IsRunning = true; + log($"=== {DateTime.Now:HH:mm:ss} {Label(verb)} {Path.GetFileName(path)} ==="); + + // Everything the worker needs is read here, on the caller's thread, so a later ribbon change + // cannot alter a run already under way. + var defragOptions = verb == MaintenanceVerb.Defragment ? this.BuildDefragOptions(ev => progress(FromEvent(ev)), token) : null; + var seed = MaintenanceInput.ParseSeed(this.SeedText).Value; + var minimal = this.MinimalGeometry && this.Capabilities.MinimalGeometry.Supported; + var placement = this.ChunkPlacement; + + return Task.Run(() => { + var clock = Stopwatch.StartNew(); + MaintenanceOutcome outcome; + try { + outcome = verb switch { + MaintenanceVerb.Defragment => this.RunDefragment(path, defragOptions!, log), + MaintenanceVerb.Optimize => this.RunOptimize(path, placement, progress, log, token), + MaintenanceVerb.Shrink => this.RunShrink(path), + MaintenanceVerb.WipeEmpty => this.RunWipe(path), + MaintenanceVerb.Purge => this.RunPurge(path), + MaintenanceVerb.Compact => RunCompact(path, minimal, log), + MaintenanceVerb.Scramble => this.RunScramble(path, seed, progress), + _ => new(MaintenanceOutcomeKind.Refused, $"{verb} is not an operation.", false), + }; + } catch (OperationCanceledException) { + outcome = this.IsStaged + ? new(MaintenanceOutcomeKind.Cancelled, "Cancelled — the staged copy was discarded; the image is unchanged.", false) + : new(MaintenanceOutcomeKind.Cancelled, "Cancelled — moves already completed stay where they are; the contents are unchanged.", true); + } catch (NotSupportedException ex) { + outcome = new(MaintenanceOutcomeKind.Refused, ex.Message, false); + } catch (Exception ex) { + outcome = new(MaintenanceOutcomeKind.Failed, $"{ex.GetType().Name}: {ex.Message}", false); + } finally { + this.IsRunning = false; + } + + log($"{outcome.Kind.ToString().ToUpperInvariant()} ({clock.ElapsedMilliseconds} ms) — {outcome.Summary}"); + return outcome; + }, CancellationToken.None); + } + + internal static string Label(MaintenanceVerb verb) => verb switch { + MaintenanceVerb.WipeEmpty => "Clearing free space in", + MaintenanceVerb.Defragment => "Defragmenting", + MaintenanceVerb.Optimize => "Optimizing", + MaintenanceVerb.Shrink => "Shrinking", + MaintenanceVerb.Purge => "Purging", + MaintenanceVerb.Compact => "Compacting", + MaintenanceVerb.Scramble => "Scrambling", + _ => verb.ToString(), + }; + + private static MaintenanceProgress FromEvent(DefragProgressEvent ev) + => new(ev.BlockMap, ev.ImageSize, ev.CurrentReadOffset, ev.CurrentWriteOffset, ev.Fraction, ev.Status, CommitStarted: ev.Phase == "committing"); + + private MaintenanceOutcome RunDefragment(string path, DefragOptions options, Action log) { + if (this.Operations is not IArchiveDefragmentable defragmentable) + throw new NotSupportedException($"{this.FormatId} has no defragmenter."); + + log($"Mode: {options.Mode}{(options.InterleaveStride > 1 ? $", interleave {options.InterleaveStride}" : "")}" + + $"{(options.MetadataZonePlacement != MetadataZone.Unchanged ? $", metadata {options.MetadataZonePlacement}" : "")}" + + $"{(options.LayoutTemplate is { } t ? $", profile {t.Name}" : "")}" + + $"{(options.Mode == DefragMode.CarveHole ? $", hole {options.HoleSize:N0} bytes at {(options.HoleAt < 0 ? "auto" : options.HoleAt.ToString("N0"))}" : "")}"); + + string? finalStatus = null; + var reporting = options with { + OnProgress = ev => { + if (ev.Phase == "complete" && !string.IsNullOrWhiteSpace(ev.Status)) finalStatus = ev.Status; + options.OnProgress?.Invoke(ev); + }, + }; + + using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) + defragmentable.Defragment(stream, reporting); + + return new(MaintenanceOutcomeKind.Succeeded, finalStatus ?? "Defragmented; size and contents unchanged.", true); + } + + private MaintenanceOutcome RunOptimize(string path, MetadataPlacementProfile? placement, Action progress, Action log, CancellationToken token) { + var originalSize = new FileInfo(path).Length; + switch (this.Capabilities.OptimizeRoute) { + case OptimizeRoute.FileInternal: { + if (this.Operations is not IFileInternalChunkMover mover) throw new NotSupportedException($"{this.FormatId} has no chunk mover."); + using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) + mover.Optimize(stream, placement); + return new(MaintenanceOutcomeKind.Succeeded, SizeChange(originalSize, new FileInfo(path).Length), true); + } + + case OptimizeRoute.CompressedVolume: { + var descriptor = FormatRegistry.GetById(this.FormatId) ?? throw new NotSupportedException($"{this.FormatId} is not registered."); + var result = CvfOptimizer.Optimize(path, descriptor); + log($"Re-encoded via {result.MethodUsed}; {result.FilesCompressed} cluster(s) compressed, {result.FilesStoredVerbatim} stored verbatim."); + return new(MaintenanceOutcomeKind.Succeeded, SizeChange(result.OriginalSize, result.OptimizedSize), true); + } + + case OptimizeRoute.SolidBlocks: + return this.RunSolidBlocks(path, originalSize, progress, log, token); + + case OptimizeRoute.Reencode: + return this.RunReencode(path, originalSize, progress, token); + + default: + throw new NotSupportedException($"{this.FormatId} has no optimizer."); + } + } + + private MaintenanceOutcome RunReencode(string path, long originalSize, Action progress, CancellationToken token) { + var staged = path + ".opt.tmp"; + AtomicFileWriter.TryDelete(staged); + if (this.Operations is { } ops && BlockMapSnapshot.StagedProjection(path, ops, originalSize) is { } projection) + progress(new(projection, originalSize, 0, -1, 0, "Staged rebuild — green: source read; orange: staged bytes written.")); + + try { + var worker = Task.Run(() => ArchiveOperations.Optimize(path, staged, password: null), CancellationToken.None); + while (!worker.Wait(100)) { + var written = FindStagedOutputLength(staged); + var fraction = originalSize > 0 ? Math.Clamp((double)written / originalSize, 0, 0.95) : 0; + var span = Math.Max(1L, originalSize); + progress(new(null, originalSize, Math.Clamp((long)(fraction * span), 0, span - 1), written > 0 ? Math.Clamp(written, 0, span - 1) : -1, fraction, + token.IsCancellationRequested + ? "Cancellation pending — the current unit finishes, then the staged copy is discarded." + : $"Rebuilding a staged copy — {MaintenanceInput.FormatSize(written)} written; the original is unchanged.")); + } + + var result = worker.GetAwaiter().GetResult(); + token.ThrowIfCancellationRequested(); + progress(new(null, originalSize, -1, -1, 0.99, "Staged copy complete — committing.", CommitStarted: true)); + AtomicFileWriter.ReplaceTarget(staged, path); + return new(MaintenanceOutcomeKind.Succeeded, $"{result.EntriesOptimized} entries re-encoded; {SizeChange(originalSize, result.OptimizedSize)}", true); + } finally { + AtomicFileWriter.TryDelete(staged); + } + } + + private MaintenanceOutcome RunSolidBlocks(string path, long originalSize, Action progress, Action log, CancellationToken token) { + FileFormat.SevenZip.SolidBlockOptimizer.OptimizeResult result; + using (var source = File.OpenRead(path)) + result = FileFormat.SevenZip.SolidBlockOptimizer.Optimize( + source, + maxTrials: 5, + onProgress: (index, total, name) => log($" Trying strategy {index + 1}/{total}: {name}..."), + onDetailedProgress: detail => { + var span = Math.Max(1L, originalSize); + var share = detail.Phase == "extracting" ? detail.BytesDone / Math.Max(1.0, detail.BytesTotal) : detail.Current / Math.Max(1.0, detail.Total); + var (fraction, read, write) = detail.Phase switch { + "extracting" => (0.30 * share, Math.Clamp((long)(share * span), 0, span - 1), -1L), + "strategy" => (0.30 + 0.10 * share, -1L, -1L), + "building" => (0.40 + 0.55 * share, -1L, Math.Clamp((long)(share * span), 0, span - 1)), + _ => (0.0, -1L, -1L), + }; + progress(new(null, originalSize, read, write, Math.Clamp(fraction, 0, 0.95), detail.Phase switch { + "extracting" => $"Reading source entry: {detail.Name}", + "strategy" => $"Planning solid grouping: {detail.Name}", + "building" => $"Building staged solid candidate: {detail.Name}", + _ => "Staged 7z regrouping", + })); + }, + cancellationToken: token); + + foreach (var trial in result.Trials) + log($" {trial.StrategyName}: {MaintenanceInput.FormatSize(trial.OutputSize)} ({trial.Elapsed.TotalMilliseconds:F0} ms)"); + + var newSize = (long)result.Data.Length; + if (newSize >= originalSize) + return new(MaintenanceOutcomeKind.Succeeded, "No grouping beat the original; the archive is unchanged.", false); + + progress(new(null, originalSize, -1, -1, 0.99, "Winning layout selected — committing.", CommitStarted: true)); + AtomicFileWriter.WriteAllBytesAtomic(path, result.Data); + return new(MaintenanceOutcomeKind.Succeeded, $"winner {result.WinningStrategy}; {SizeChange(originalSize, newSize)}", true); + } + + private MaintenanceOutcome RunShrink(string path) { + var originalSize = new FileInfo(path).Length; + switch (this.FormatId) { + case "Fat": { + using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); + var result = FileSystem.Fat.FatShrinkHelper.Shrink(stream); + return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); + } + case "Ext" or "Ext1": { + using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); + var result = FileSystem.Ext.ExtShrinkHelper.Shrink(stream); + return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); + } + case "Vhd": { + using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); + var result = FileFormat.Vhd.VhdCompactor.Compact(stream); + return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); + } + } + + if (this.Operations is not IArchiveShrinkable shrinkable) throw new NotSupportedException($"{this.FormatId} cannot shrink."); + + // Shrink into a staged file, then swap it in, so a crash mid-write cannot corrupt the source. + var staged = path + ".shrink.tmp"; + try { + using (var input = File.OpenRead(path)) + using (var output = File.Create(staged)) + shrinkable.Shrink(input, output); + + var newSize = new FileInfo(staged).Length; + AtomicFileWriter.ReplaceTarget(staged, path); + return Shrunk(newSize < originalSize, originalSize, newSize); + } finally { + AtomicFileWriter.TryDelete(staged); + } + + static MaintenanceOutcome Shrunk(bool reduced, long before, long after) + => new(MaintenanceOutcomeKind.Succeeded, reduced ? SizeChange(before, after) : "Already compact; nothing to trim.", reduced); + } + + private MaintenanceOutcome RunWipe(string path) { + var ops = this.Operations; + var totalUnused = -1L; + long wiped; + using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) { + switch (ops) { + // Every extent and layout map is a wiper too; the interface default wipes through the map. + case IWipeEmpty wiper: + // The wiper reports only the bytes it had to overwrite; on a mostly empty image that and the + // free space diverge sharply, so the total is worked out first where the layout allows. + if (ops is IFilesystemExtentMap fsMap) totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(fsMap.EnumerateExtents(stream), stream.Length); + else if (ops is IArchiveLayoutMap arMap) totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(arMap.EnumerateLayout(stream), stream.Length); + stream.Position = 0; + wiped = wiper.WipeUnusedSpace(stream); + break; + default: + throw new NotSupportedException($"{this.FormatId} exposes no layout to tell free space from live data."); + } + } + + var summary = totalUnused >= 0 + ? $"{MaintenanceInput.FormatSize(totalUnused)} unused; {MaintenanceInput.FormatSize(wiped)} newly zeroed, {MaintenanceInput.FormatSize(Math.Max(0, totalUnused - wiped))} already zero." + : $"{MaintenanceInput.FormatSize(wiped)} zeroed."; + return new(MaintenanceOutcomeKind.Succeeded, summary, wiped > 0); + } + + private MaintenanceOutcome RunPurge(string path) { + var ops = this.Operations; + if (ops is not IArchiveModifiable) throw new NotSupportedException($"{this.FormatId} cannot remove entries."); + + List entries; + using (var probe = File.OpenRead(path)) + entries = ops.List(probe, password: null); + if (entries.Count == 0) return new(MaintenanceOutcomeKind.Succeeded, "The container is already empty.", false); + + var originalSize = new FileInfo(path).Length; + ArchiveOperations.Remove(path, [.. entries.Select(e => e.Name)]); + return new(MaintenanceOutcomeKind.Succeeded, $"{entries.Count(e => !e.IsDirectory)} file(s) erased; {SizeChange(originalSize, new FileInfo(path).Length)}", true); + } + + private static MaintenanceOutcome RunCompact(string path, bool minimal, Action log) { + var result = CompactOperation.Compact(path, new CompactOperation.CompactOptions { Minimal = minimal, Log = line => log(" " + line) }); + var steps = result.StepsRun.Count > 0 ? string.Join(", ", result.StepsRun) : "none"; + return new(MaintenanceOutcomeKind.Succeeded, $"steps: {steps}; {SizeChange(result.OriginalSize, result.NewSize)}", result.StepsRun.Count > 0); + } + + private MaintenanceOutcome RunScramble(string path, int seed, Action progress) { + if (this.Operations is not IFilesystemScrambleable scrambler) throw new NotSupportedException($"{this.FormatId} cannot scatter a volume in place."); + + using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) + scrambler.Scramble(stream, new ScrambleOptions { Seed = seed, OnProgress = ev => progress(FromEvent(ev)) }); + return new(MaintenanceOutcomeKind.Succeeded, $"seed {seed}: the volume is fragmented; Defragment puts it back.", true); + } + + private static string SizeChange(long before, long after) { + var pct = before > 0 ? 100.0 * (after - before) / before : 0; + return $"{MaintenanceInput.FormatSize(before)} → {MaintenanceInput.FormatSize(after)} ({pct:+0.0;-0.0;0.0}%)"; + } + + /// + /// The optimizer writes through its own temporary file, so the staged size is the largest of the + /// target and any sibling scratch file it is currently filling. + /// + private static long FindStagedOutputLength(string target) { + try { + var best = File.Exists(target) ? new FileInfo(target).Length : 0L; + var directory = Path.GetDirectoryName(target); + if (string.IsNullOrEmpty(directory)) directory = Directory.GetCurrentDirectory(); + foreach (var candidate in Directory.EnumerateFiles(directory, Path.GetFileName(target) + ".tmp.*")) + best = Math.Max(best, new FileInfo(candidate).Length); + return best; + } catch { + return 0; + } + } +} diff --git a/Compression.NativeUI/Maintenance/MaintenanceVerb.cs b/Compression.NativeUI/Maintenance/MaintenanceVerb.cs new file mode 100644 index 000000000..abc115dfd --- /dev/null +++ b/Compression.NativeUI/Maintenance/MaintenanceVerb.cs @@ -0,0 +1,71 @@ +namespace Compression.NativeUI.Maintenance; + +/// +/// The maintenance operations the Defragment tab runs: the five canonical verbs from +/// docs/ARCHIVE-MODEL.md plus the two composites. +/// +internal enum MaintenanceVerb { + /// Find and apply the best layout, or re-encode the payload; size preserved where possible. + Optimize, + + /// Keep the parameter set; minimise the stored footprint. + Shrink, + + /// Re-order entries and extents so files are contiguous; size preserved. + Defragment, + + /// Erase all live data, leaving a valid empty container. + Purge, + + /// Overwrite only unused space — free clusters, slack, deleted entries; size preserved. + WipeEmpty, + + /// Defragment, optimize and shrink in one pass: the smallest valid container. + Compact, + + /// + /// Scatter every allocation block across the volume — fragmentation on purpose, so + /// has something real to undo. Content is preserved; only its place changes. + /// + Scramble, +} + +/// +/// How a defragmentation lays the volume out, in the words the ribbon uses. Each maps onto one +/// engine , except , which no +/// descriptor offers yet and is answered by the capability query rather than by a placeholder. +/// +internal enum DefragStrategy { + /// Pack every file at one end of the volume (ConsolidateAtStart, or ConsolidateAtEnd). + Consolidate, + + /// Fill existing holes from the tail, moving as little as possible (FillHolesLazy). + Defrag, + + /// Move only what reads backwards, so every file ascends (AscendingOrder). + Reorder, + + /// Sort the entries of every directory. No descriptor offers this today. + SortEntries, + + /// Reserve one contiguous free region of a chosen size (CarveHole). + CarveHole, +} + +/// Which engine an optimize runs through for a given format. +internal enum OptimizeRoute { + /// The format has no optimizer. + None, + + /// Chunks inside one file are reordered in place (MP4 fast-start, PNG/JPEG metadata). + FileInternal, + + /// A compressed-volume file's clusters are re-encoded (DoubleSpace, DriveSpace). + CompressedVolume, + + /// A 7z archive's solid blocks are regrouped; the smallest candidate wins. + SolidBlocks, + + /// The container is re-encoded with its strongest settings into a staged copy. + Reencode, +} diff --git a/Compression.NativeUI/ScreenshotMode.cs b/Compression.NativeUI/ScreenshotMode.cs index f3d4cbde7..25900029e 100644 --- a/Compression.NativeUI/ScreenshotMode.cs +++ b/Compression.NativeUI/ScreenshotMode.cs @@ -44,8 +44,14 @@ public static Form CreateWindow(string target, string fixtureRoot) { return window; } - case "maintenance": - return new DefragmentWindow(CreateFilesystemFixture(fixtureRoot)); + case "maintenance": { + // The shell on the Defragment tab, so the capture shows the ribbon and the block view the + // way a user reaches them. + var window = new MainForm([]); + window.OpenArchive(CreateFilesystemFixture(fixtureRoot)); + window.OpenDefragmentTab(verb: null); + return window; + } default: throw new ArgumentOutOfRangeException(nameof(target), target, diff --git a/Compression.NativeUI/ViewModels/MainViewModel.cs b/Compression.NativeUI/ViewModels/MainViewModel.cs index 584894f0c..be9a0f11e 100644 --- a/Compression.NativeUI/ViewModels/MainViewModel.cs +++ b/Compression.NativeUI/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using Compression.Lib; +using Compression.NativeUI.Maintenance; using Compression.NativeUI.Views; using Compression.Registry; using Hawkynt.NativeForms; @@ -119,6 +120,9 @@ internal sealed class MainViewModel : ViewModelBase { public ICommand WipeEntryCommand { get; } public ICommand CompactEntryCommand { get; } public ICommand ScrambleEntryCommand { get; } + + /// Opens the Defragment tab on the resolved maintenance target, with no operation preselected. + public ICommand MaintenanceCommand { get; } public ICommand ReconfigureEntryCommand { get; } public ICommand DeleteSelectedCommand { get; } /// Asks the view to start editing the selected entry's name; see . @@ -224,6 +228,7 @@ public MainViewModel() { WipeEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.WipeEmpty), _ => CanMaintain(MaintenanceVerb.WipeEmpty)); CompactEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.Compact), _ => CanMaintain(MaintenanceVerb.Compact)); ScrambleEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.Scramble), _ => CanMaintain(MaintenanceVerb.Scramble)); + MaintenanceCommand = new RelayCommand(_ => MaintenanceRequested?.Invoke(this, null), _ => HasMaintenanceTarget); ReconfigureEntryCommand = new RelayCommand(_ => Reconfigure(), _ => CanReconfigure()); DeleteSelectedCommand = new RelayCommand(_ => DeleteSelectedEntries(), _ => CanDeleteSelected); CopyCommand = new RelayCommand(_ => PutSelectionOnClipboard(cut: false), _ => CanCopySelection()); @@ -855,8 +860,9 @@ private bool TryResolveMaintenanceTarget(out string formatId, out ArchiveEntryVi } } - // Fall back to the open archive itself. - if (HasArchive && !string.IsNullOrEmpty(ArchivePath) && !string.IsNullOrEmpty(Format) + // Fall back to the open archive itself — while the shell is in it; a folder on disk being + // browsed is not the archive left behind. + if (HasArchive && !IsBrowsingOsFolder && !string.IsNullOrEmpty(ArchivePath) && !string.IsNullOrEmpty(Format) && Format != FormatDetector.Format.Unknown.ToString()) { formatId = Format; return true; @@ -865,95 +871,88 @@ private bool TryResolveMaintenanceTarget(out string formatId, out ArchiveEntryVi } /// - /// True when the resolved maintenance target's descriptor implements the - /// capability interface backing . Mirrors the action - /// availability inside so a context-menu - /// item is never offered for an op the window can't actually run. + /// True when the resolved maintenance target supports . Asks the same + /// capability adapter the Defragment tab enables its ribbon from, so a context-menu item is never + /// offered for an operation the tab would then refuse. /// - private bool CanMaintain(MaintenanceVerb verb) { - if (!TryResolveMaintenanceTarget(out var formatId, out _)) return false; - var ops = FormatRegistry.GetArchiveOps(formatId); - if (ops == null) return false; - return verb switch { - MaintenanceVerb.Optimize => ops is IArchiveCreatable or IFileInternalChunkMover, - MaintenanceVerb.Shrink => ops is IArchiveShrinkable || formatId is "Fat" or "Ext" or "Ext1" or "Vhd", - MaintenanceVerb.Defragment => ops is IArchiveDefragmentable, - MaintenanceVerb.Purge => ops is IArchiveModifiable, - MaintenanceVerb.WipeEmpty => ops is IWipeEmpty or IFilesystemExtentMap or IArchiveLayoutMap, - MaintenanceVerb.Compact => ops is IArchiveDefragmentable or IArchiveShrinkable or IArchiveCreatable, - MaintenanceVerb.Scramble => ops is IFilesystemScrambleable, - _ => false, - }; - } + private bool CanMaintain(MaintenanceVerb verb) + => TryResolveMaintenanceTarget(out var formatId, out _) && MaintenanceCapabilities.For(formatId).Verb(verb).Supported; + + /// Whether there is a maintenance target that supports at least one operation. + internal bool HasMaintenanceTarget + => TryResolveMaintenanceTarget(out var formatId, out _) && MaintenanceCapabilities.For(formatId).AnySupported; /// - /// Opens pre-targeted at - /// against the resolved target. For an archive nested - /// inside the open archive, the entry is extracted to a temp file, maintained - /// there, and (when the host is ) written back - /// via . + /// Identifies the resolved maintenance target — the selected archive file or entry, else the open + /// archive — so the view can tell whether a session still belongs to what is selected. Null when + /// there is none. /// - private void OpenMaintenance(MaintenanceVerb verb) { - if (!TryResolveMaintenanceTarget(out _, out var entry)) return; + internal string? MaintenanceTargetKey + => TryResolveMaintenanceTarget(out var formatId, out var entry) + ? $"{formatId}|{(entry is null ? ArchivePath : IsBrowsingOsFolder ? entry.Path : ArchivePath + "::" + entry.Path)}" + : null; - string targetPath; - Action? cleanup = null; - Action? writeBack = null; + /// + /// Raised when a maintenance command asks for the Defragment tab, with the operation to preselect + /// (null for none). The view owns the tab, so it is the one that opens it. + /// + public event EventHandler? MaintenanceRequested; + + private void OpenMaintenance(MaintenanceVerb verb) => MaintenanceRequested?.Invoke(this, verb); + + /// + /// Opens the resolved target for maintenance. A real file — on disk, or the open archive — is + /// maintained in place. An archive nested inside the open archive is extracted to a temporary file, + /// maintained there, and written back into the host after each change when the host is + /// ; the session deletes the copy when disposed. + /// + internal MaintenanceSession? OpenMaintenanceSession() { + if (!TryResolveMaintenanceTarget(out var formatId, out var entry) || MaintenanceTargetKey is not { } key) return null; if (entry != null && IsBrowsingOsFolder) { - // Real on-disk archive file — maintained in place. - targetPath = entry.Path; - if (string.IsNullOrEmpty(targetPath) || !File.Exists(targetPath)) return; - } else if (entry != null) { - // Nested archive entry inside the currently-open archive: materialise → - // maintain → write back into the host (if the host supports modification). + if (string.IsNullOrEmpty(entry.Path) || !File.Exists(entry.Path)) return null; + return new(key, entry.Path, formatId, entry.Name, _ => RefreshVisibleEntries(), cleanup: null); + } + + if (entry != null) { try { var bytes = ArchiveOperations.ExtractEntry(ArchivePath, entry.Path, password: null); - var temp = Path.Combine(Path.GetTempPath(), - $"cwb_maint_{Guid.NewGuid():N}_{Path.GetFileName(entry.Name)}"); + var temp = Path.Combine(Path.GetTempPath(), $"cwb_maint_{Guid.NewGuid():N}_{Path.GetFileName(entry.Name)}"); File.WriteAllBytes(temp, bytes); - targetPath = temp; - cleanup = () => { try { File.Delete(temp); } catch { /* best effort */ } }; var hostPath = ArchivePath; var entryName = entry.Path; var entryLabel = entry.Name; - if (FormatRegistry.GetArchiveOps(Format) is IArchiveModifiable) { - writeBack = () => { - try { - ArchiveOperations.Replace(hostPath, entryName, temp); - StatusText = $"Wrote maintained '{entryLabel}' back into {Path.GetFileName(hostPath)}."; - } catch (Exception ex) { - StatusText = $"Could not write '{entryLabel}' back into host archive: {ex.Message}"; - } - }; - } else { - StatusText = $"'{Format}' is read-only — '{entryLabel}' will be maintained in a copy that is not written back."; - } + var writable = FormatRegistry.GetArchiveOps(Format) is IArchiveModifiable; + if (!writable) + StatusText = $"'{Format}' is read-only — '{entryLabel}' is maintained in a copy that is not written back."; + + return new(key, temp, formatId, entryLabel, _ => { + if (!writable) return; + try { + ArchiveOperations.Replace(hostPath, entryName, temp); + StatusText = $"Wrote maintained '{entryLabel}' back into {Path.GetFileName(hostPath)}."; + } catch (Exception ex) { + StatusText = $"Could not write '{entryLabel}' back into host archive: {ex.Message}"; + } + + if (HasArchive) ReloadArchiveInPlace(); + }, cleanup: () => { + try { File.Delete(temp); } catch { /* best effort */ } + }); } catch (Exception ex) { StatusText = $"Cannot open nested archive '{entry.Name}': {ex.Message}"; - return; + return null; } - } else { - // The currently-open archive itself (top-level real file, or a descended - // nested temp — the latter mutates the temp only, matching the existing - // nested-edit limitation). - if (string.IsNullOrEmpty(ArchivePath) || !File.Exists(ArchivePath)) return; - targetPath = ArchivePath; } - var dlg = new DefragmentWindow(targetPath, verb); - dlg.ArchiveMutated += mutated => { - writeBack?.Invoke(); - if (writeBack != null && HasArchive) - ReloadArchiveInPlace(); // nested write-back → re-list the host - else if (HasArchive && string.Equals(mutated, ArchivePath, StringComparison.OrdinalIgnoreCase)) - ReloadArchiveInPlace(); - else if (IsBrowsingOsFolder) - RefreshVisibleEntries(); - }; - dlg.FormClosed += (_, _) => cleanup?.Invoke(); - dlg.Show(); + // The open archive itself: a top-level file, or a descended nested temp — the latter mutates the + // temp only, matching the existing nested-edit limitation. + if (string.IsNullOrEmpty(ArchivePath) || !File.Exists(ArchivePath)) return null; + var archivePath = ArchivePath; + return new(key, archivePath, formatId, Path.GetFileName(archivePath), mutated => { + if (HasArchive && string.Equals(mutated, ArchivePath, StringComparison.OrdinalIgnoreCase)) ReloadArchiveInPlace(); + }, cleanup: null); } /// diff --git a/Compression.NativeUI/ViewModels/MaintenanceSession.cs b/Compression.NativeUI/ViewModels/MaintenanceSession.cs new file mode 100644 index 000000000..778e69b0a --- /dev/null +++ b/Compression.NativeUI/ViewModels/MaintenanceSession.cs @@ -0,0 +1,32 @@ +namespace Compression.NativeUI.ViewModels; + +/// +/// One maintenance target opened for the Defragment tab: the file the operations work on, its format, +/// what to do after the file changed, and what to clean up afterwards. A nested archive is worked on +/// as a temporary copy; disposing the session deletes it. +/// +internal sealed class MaintenanceSession( + string key, + string imagePath, + string formatId, + string displayName, + Action onMutated, + Action? cleanup) : IDisposable { + private Action? _cleanup = cleanup; + + /// Identifies the target, as does. + public string Key { get; } = key; + + /// The file the operations read and write. + public string ImagePath { get; } = imagePath; + + public string FormatId { get; } = formatId; + + /// The name the user knows the target by. + public string DisplayName { get; } = displayName; + + /// Tells the shell the file changed, so it re-lists it and writes a nested copy back. + public void NotifyMutated() => onMutated(this.ImagePath); + + public void Dispose() => Interlocked.Exchange(ref this._cleanup, null)?.Invoke(); +} diff --git a/Compression.NativeUI/Views/DefragmentView.cs b/Compression.NativeUI/Views/DefragmentView.cs new file mode 100644 index 000000000..066b7766c --- /dev/null +++ b/Compression.NativeUI/Views/DefragmentView.cs @@ -0,0 +1,218 @@ +using System.Drawing; +using Compression.NativeUI.Controls; +using Compression.NativeUI.Maintenance; +using Compression.Registry; +using Hawkynt.NativeForms; +using Hawkynt.NativeForms.Drawing; + +namespace Compression.NativeUI.Views; + +/// +/// The client area of the Defragment tab: the block map filling the window, an optional files panel +/// beside it, the colour key under it and one status line with the progress of whatever is running. +/// It only shows what it is given; decides what that is. +/// +internal sealed class DefragmentView : Panel { + private const int StatusHeight = 22; + private const int LegendHeight = 40; + private const int ProgressHeight = 4; + private const int FilesPanelWidth = 340; + private const int LogHeight = 120; + + private static readonly Font MonoFont = new("Cascadia Mono", 8f, FontStyle.Regular); + + private readonly Label _status = new() { Text = "—", Dock = DockStyle.Top, Height = StatusHeight }; + private readonly SplitContainer _split = new() { FixedPanel = FixedPanel.Panel2, Panel1MinSize = 160, Panel2MinSize = 200, Dock = DockStyle.Fill }; + private readonly Label _filesStatus = new() { Text = "—", ForeColor = Color.DimGray, Dock = DockStyle.Top, Height = 18 }; + private readonly DataGridView _files = new() { ReadOnly = true, ShowGridLines = true, Dock = DockStyle.Fill }; + private readonly TextBox _log = new() { ReadOnly = true, Multiline = true, Font = MonoFont, Dock = DockStyle.Bottom, Height = LogHeight }; + private readonly LegendStrip _legend = new() { Dock = DockStyle.Bottom, Height = LegendHeight }; + private readonly ProgressBar _progress = new() { Minimum = 0, Maximum = 100, Dock = DockStyle.Bottom, Height = ProgressHeight }; + private TileContentsWindow? _tileWindow; + private bool _splitSized; + + public DefragmentView() { + this.BackColor = DefaultTheme.Instance.ControlBackground; + this.Map.TileClicked += this.OnTileClicked; + + AddColumn("Name", static o => ((FileRow)o!).Name, 150); + AddColumn("Size", static o => ((FileRow)o!).SizeDisplay, 70, (a, b) => ((FileRow)a!).Size.CompareTo(((FileRow)b!).Size)); + var fragments = AddColumn("Frag", static o => ((FileRow)o!).FragmentsDisplay, 40); + fragments.Alignment = ContentAlignment.MiddleCenter; + fragments.TooltipSelector = static _ => "How many separate stretches of the container this file's data occupies. 1 is contiguous; higher is fragmented. An em dash means the format offers no layout to count from."; + AddColumn("Method", static o => ((FileRow)o!).MethodDisplay, 70); + AddColumn("Modified", static o => ((FileRow)o!).ModifiedDisplay, 110, (a, b) => Nullable.Compare(((FileRow)a!).Modified, ((FileRow)b!).Modified)); + AddColumn("Class", static o => ((FileRow)o!).Class, 56); + + // Docked children claim their edges in reverse order of addition: the status line takes the + // top, the progress line the very bottom, the colour key sits above it and the map fills the rest. + this.Map.Dock = DockStyle.Fill; + this._split.Panel1.Controls.Add(this.Map); + this._split.Panel2.Controls.AddRange(this._files, this._log, this._filesStatus); + this.Controls.AddRange(this._split, this._legend, this._progress, this._status); + + DataGridViewColumn AddColumn(string header, Func selector, int width, Comparison? sort = null) { + var column = new DataGridViewColumn(header, selector) { Width = width }; + if (sort is not null) column.SortComparison = sort; + this._files.Columns.Add(column); + return column; + } + } + + /// The block map itself. + public BlockMapControl Map { get; } = new(); + + /// The line above the map: how honest the picture is, or what the running operation is doing. + public string StatusText { + get => this._status.Text; + set => this._status.Text = value; + } + + /// Whether the files panel shows beside the map. + public bool FilesPanelVisible { + get => !this._split.Panel2Collapsed; + set => this._split.Panel2Collapsed = !value; + } + + /// Whether the colour key shows under the map. + /// + /// Kept as its own flag: reports the effective state, which is false + /// for as long as the whole view is hidden behind the browser. + /// + public bool LegendVisible { + get => this._legendVisible; + set { + this._legendVisible = value; + this._legend.Visible = value; + this.PerformLayout(); + } + } + + private bool _legendVisible = true; + + /// Every line logged since the view was last reset. + public string LogText => this._log.Text; + + public int ProgressValue { + get => this._progress.Value; + set => this._progress.Value = Math.Clamp(value, 0, 100); + } + + /// Shows an image at rest: its layout, its files and how the layout was obtained. + public void ShowSnapshot(BlockMapSnapshot snapshot) { + this.Map.ReadHead = -1; + this.Map.WriteHead = -1; + this.Map.BlockMap = snapshot.Map; + this.Map.ImageSize = snapshot.ImageSize; + this._files.DataSource = snapshot.Rows; + this._filesStatus.Text = snapshot.Rows.Count switch { + 0 => "—", + 1 => "1 file", + var n => $"{n:N0} files", + }; + this.StatusText = snapshot.Status; + } + + /// Moves the picture along with a running operation. + public void Apply(MaintenanceProgress progress) { + if (progress.Map is not null) { + this.Map.BlockMap = progress.Map; + this.Map.ImageSize = progress.ImageSize; + } else if (progress.ImageSize > 0 && this.Map.ImageSize <= 0) { + this.Map.ImageSize = progress.ImageSize; + } + + this.Map.ReadHead = progress.ReadHead; + this.Map.WriteHead = progress.WriteHead; + if (progress.Fraction >= 0) this.ProgressValue = (int)(Math.Clamp(progress.Fraction, 0, 1) * 100); + if (!string.IsNullOrWhiteSpace(progress.Status)) this.StatusText = progress.Status!; + } + + public void Log(string line) { + this._log.AppendText(line + Environment.NewLine); + this._log.SelectionStart = this._log.Text.Length; + } + + /// Forgets the previous image: no map, no rows, no log. + public void Clear() { + this.ShowSnapshot(BlockMapSnapshot.Empty("—")); + this._log.Text = ""; + this.ProgressValue = 0; + } + + /// + /// Opens the files panel at a fixed width, once, when the view first has a real size; from then on + /// the map takes whatever the window gains, and a width the user dragged to is theirs. + /// + public void SizeFilesPanelOnce() { + if (this._splitSized || this.Width <= FilesPanelWidth + this._split.Panel1MinSize) return; + this._splitSized = true; + this._split.SplitterDistance = this.Width - FilesPanelWidth - this._split.SplitterWidth; + } + + /// + /// Opens a modeless drill-down listing every block under the clicked tile. Modeless so the user can + /// keep clicking tiles to compare; each click re-targets the same window. + /// + private void OnTileClicked(object? sender, TileClickedEventArgs e) { + if (this._tileWindow is null) { + this._tileWindow = new TileContentsWindow(); + this._tileWindow.FormClosed += (_, _) => this._tileWindow = null; + } + + this._tileWindow.SetContents(e.StartOffset, e.EndOffset, e.Contents); + this._tileWindow.Show(); + } + + /// The block-map colour key. + private sealed class LegendStrip : OwnerDrawnControl { + private static readonly (Color Color, string Label)[] Swatches = [ + (Color.FromArgb(0xE6, 0x4A, 0x19), "Hot"), + (Color.FromArgb(0x42, 0xA5, 0xF5), "Normal"), + (Color.FromArgb(0x66, 0xBB, 0x6A), "Cold"), + (Color.FromArgb(0x90, 0xA4, 0xAE), "Frozen"), + (Color.FromArgb(0xDA, 0xA5, 0x20), "Directory"), + (Color.FromArgb(0xE0, 0xE0, 0xE0), "Free"), + (Color.FromArgb(0xC0, 0x30, 0x30), "Bad"), + (Color.FromArgb(0x70, 0x70, 0x70), "Reserved"), + ]; + + protected override void OnPaint(PaintEventArgs e) { + var g = e.Graphics; + var theme = DefaultTheme.Instance; + var font = theme.DefaultFont; + g.FillRectangle(theme.ControlBackground, new(0, 0, this.Width, this.Height)); + + // Rows and labels are measured, so the key stays whole at any display scaling. + var row = Math.Max(16, g.MeasureText("Ag", font).Height); + var x = 0; + var y = 2; + foreach (var (color, label) in Swatches) { + var text = g.MeasureText(label, font).Width; + if (!this.Fit(ref x, ref y, 14 + text + 12, row)) return; + g.FillRectangle(color, new(x, y + (row - 10) / 2, 10, 10)); + g.DrawText(label, font, theme.ControlText, new(x + 14, y, text + 4, row), ContentAlignment.MiddleLeft); + x += 14 + text + 12; + } + + // The two heads are lines rather than swatches, matching how they are drawn on the map. + foreach (var (color, label) in new[] { (Color.LimeGreen, "Read head"), (Color.Orange, "Write head") }) { + var text = g.MeasureText(label, font).Width; + if (!this.Fit(ref x, ref y, 8 + text + 12, row)) return; + g.DrawLine(color, x + 2, y + 2, x + 2, y + row - 2, 2); + g.DrawText(label, font, theme.ControlText, new(x + 8, y, text + 4, row), ContentAlignment.MiddleLeft); + x += 8 + text + 12; + } + } + + /// Wraps to the next row when does not fit; false once out of rows. + private bool Fit(ref int x, ref int y, int width, int row) { + if (x > 0 && x + width > this.Width) { + x = 0; + y += row + 2; + } + + return y + row <= this.Height; + } + } +} diff --git a/Compression.NativeUI/Views/DefragmentWindow.cs b/Compression.NativeUI/Views/DefragmentWindow.cs deleted file mode 100644 index 764b17b4f..000000000 --- a/Compression.NativeUI/Views/DefragmentWindow.cs +++ /dev/null @@ -1,2221 +0,0 @@ -using System.Diagnostics; -using System.Drawing; -using Compression.Core.Layout; -using Compression.Lib; -using Compression.Lib.Layout; -using Compression.NativeUI.Controls; -using Compression.Registry; -using Compression.Registry.Layout; -using Hawkynt.NativeForms; -using Hawkynt.NativeForms.Drawing; - -namespace Compression.NativeUI.Views; - -/// -/// The five canonical maintenance verbs from docs/ARCHIVE-MODEL.md, plus the two composites. -/// Used to pre-focus the matching action when the window is opened from a context-menu entry. -/// -internal enum MaintenanceVerb { - /// Find and apply the best layout, or re-encode the payload; size preserved where possible. - Optimize, - - /// Keep the parameter set; minimise the stored footprint. - Shrink, - - /// Re-order entries and extents so files are contiguous; size preserved. - Defragment, - - /// Erase all live data, leaving a valid empty container. - Purge, - - /// Overwrite only unused space — free clusters, slack, deleted entries; size preserved. - WipeEmpty, - - /// Defragment, optimize and shrink in one pass: the smallest valid container. - Compact, - - /// - /// Scatter every allocation block across the volume — fragmentation on purpose, so - /// has something real to undo. Content is preserved; only its place changes. - /// - Scramble, -} - -/// -/// One row of the file-list panel: name, size, fragment count, modification date and thermal class. -/// The fragment count comes from whatever layout the format offered; formats that offer none show an -/// em dash rather than a figure nothing measured. -/// -internal sealed class FileRow { - public string Name { get; init; } = ""; - public long Size { get; init; } - public string SizeDisplay { get; init; } = ""; - public string FragmentsDisplay { get; init; } = ""; - public string MethodDisplay { get; init; } = ""; - public DateTime? Modified { get; init; } - public string ModifiedDisplay { get; init; } = ""; - public string Class { get; init; } = ""; -} - -/// -/// The single surface behind the explorer's maintenance menu: shows the detected format and what it -/// supports, lets the user pick a layout strategy, and runs the descriptor's optimize, shrink, -/// defragment, purge, wipe, compact and scramble paths through the matching capability interface. -/// -internal sealed class DefragmentWindow : Form { - /// Cap on extents fed to the block map; adjacent same-kind regions merge beyond it. - private const int MaxExtents = 50_000; - - private static readonly Color HeadingColor = Color.FromArgb(0x44, 0x66, 0xAA); - private static readonly Font MonoFont = new("Cascadia Mono", 8f, FontStyle.Regular); - - // Image picker. - private readonly GroupBox _imageBox = new() { Text = "Image" }; - private readonly TextBox _imagePathBox = new() { ReadOnly = true, Text = "Select a filesystem image to defragment...", ForeColor = Color.Gray }; - private readonly Button _browse = new() { Text = "Browse..." }; - - // Format and capability summary. - private readonly Panel _infoPanel = new() { BorderStyle = BorderStyle.FixedSingle }; - private readonly Label _formatLabel = new() { Text = "—" }; - private readonly Label _supportLabel = new() { Text = "—" }; - private readonly Label _sizeLabel = new() { Text = "—" }; - - // Filesystem layout strategy. - private readonly GroupBox _fsModesGroup = new() { Text = "Layout strategy" }; - private readonly RadioButton _modePackStart = new() { Text = "Pack at start (default)", Checked = true }; - private readonly RadioButton _modePackEnd = new() { Text = "Pack at end" }; - private readonly RadioButton _modeFillHoles = new() { Text = "Fill holes (lazy)" }; - private readonly RadioButton _modeAscending = new() { Text = "Ascending order only" }; - private readonly RadioButton _modeCarveHole = new() { Text = "Carve a hole" }; - private readonly TextBox _interleaveStride = new() { Text = "1" }; - private readonly ComboBox _metadataZone = new() { DropDownStyle = ComboBoxStyle.DropDownList }; - private readonly ComboBox _layoutProfile = new() { DropDownStyle = ComboBoxStyle.DropDownList }; - private readonly Button _editProfiles = new() { Text = "Edit profiles…" }; - - // Archive repack. - private readonly GroupBox _archiveRepackGroup = new() { Text = "Archive optimization", Visible = false }; - private readonly Label _archiveRepackNote = new() { ForeColor = Color.DimGray }; - private readonly CheckBox _smartSolidRepack = new() { Text = "Smart solid-block repack (7z only)", Visible = false }; - private readonly ComboBox _metadataPlacement = new() { DropDownStyle = ComboBoxStyle.DropDownList, Visible = false }; - private readonly Label _metadataPlacementCaption = new() { Text = "Metadata placement:", Visible = false }; - - // Carve-hole options. - private readonly GroupBox _carveOptsGroup = new() { Text = "Carve-hole options", Visible = false }; - private readonly TextBox _holeSize = new() { Text = "64m" }; - private readonly TextBox _holeAt = new() { Text = "auto" }; - - // Block map and file panel. - private readonly SplitContainer _split = new(); - private readonly GroupBox _blockMapGroup = new() { Text = "Block map" }; - private readonly Label _layoutStatus = new() { Text = "—", ForeColor = Color.DimGray }; - private readonly ComboBox _blockMapView = new() { DropDownStyle = ComboBoxStyle.DropDownList }; - private readonly BlockMapControl _blockMap = new(); - private readonly LegendStrip _legend = new(); - private readonly CheckBox _filesPanelToggle = new() { Text = "Files panel", Checked = true }; - private readonly GroupBox _filesPanelGroup = new() { Text = "Files" }; - private readonly Label _filesPanelStatus = new() { Text = "—", ForeColor = Color.DimGray }; - private readonly DataGridView _filesGrid = new() { ReadOnly = true, ShowGridLines = true }; - - // Output and progress. - private readonly GroupBox _outputGroup = new() { Text = "Output" }; - private readonly TextBox _outputBox = new() { ReadOnly = true, Multiline = true, Font = MonoFont, BackColor = Color.FromArgb(0xFA, 0xFA, 0xFA) }; - private readonly ProgressBar _progress = new() { Minimum = 0, Maximum = 100 }; - - // Actions. - private readonly TextBox _scrambleSeed = new() { Text = "1", Enabled = false }; - private readonly Button _scramble = new() { Text = "Scramble", Enabled = false }; - private readonly Button _run = new() { Text = "Defragment", Enabled = false }; - private readonly Button _cancel = new() { Text = "Cancel", Enabled = false }; - private readonly Button _close = new() { Text = "Close" }; - private readonly CheckBox _minimalGeometry = new() { Text = "Minimal geometry", Enabled = false }; - private readonly Button _compact = new() { Text = "Compact", Enabled = false }; - private readonly Button _purge = new() { Text = "Purge", Enabled = false }; - private readonly Button _wipeEmpty = new() { Text = "Wipe Empty", Enabled = false }; - private readonly Button _shrink = new() { Text = "Shrink", Enabled = false }; - private readonly ToolTip _toolTips = new(); - - private readonly List _layoutProfileEntries = []; - - private MaintenanceVerb? _requestedVerb; - private string? _imagePath; - private IArchiveDefragmentable? _defragmentable; - private IArchiveFormatOperations? _archiveOps; - private string? _formatId; - private bool _isArchiveMode; - private bool _isFileInternalMode; - private bool _isSevenZipFormat; - private IFileInternalChunkMover? _chunkMover; - private int _fileRowCount; - private string _filesSortDescription = "listing order"; - private LayoutTemplate? _selectedLayoutProfile; - private TileContentsWindow? _tileContentsWindow; - - private CancellationTokenSource? _maintenanceCancellation; - private bool _maintenanceIsStaged; - private bool _maintenanceCommitStarted; - private string? _maintenanceOperationName; - - public DefragmentWindow() { - this.Text = "Maintenance — Optimize / Shrink / Defragment / Purge / Wipe"; - this.ClientSize = new(900, 780); - this.MinimumSize = new(720, 560); - this.StartPosition = FormStartPosition.CenterParent; - - this.BuildImageBox(); - this.BuildInfoPanel(); - this.BuildFsModes(); - this.BuildArchiveRepack(); - this.BuildCarveOptions(); - this.BuildBlockMapArea(); - this.BuildOutput(); - this.BuildActions(); - - this.RefreshLayoutProfilesCombo(); - this.FormClosing += this.OnFormClosing; - this.Resize += (_, _) => this.LayoutChildren(); - this.LayoutChildren(); - } - - public DefragmentWindow(string preselectedImage) : this() { - if (!string.IsNullOrEmpty(preselectedImage) && File.Exists(preselectedImage)) - this.LoadImage(preselectedImage); - } - - /// - /// Opens the window pre-targeted at : the matching action is highlighted - /// and focused so it runs on Enter. It deliberately does not auto-run — maintenance mutates the - /// image in place, so the user confirms by clicking. - /// - public DefragmentWindow(string preselectedImage, MaintenanceVerb verb) : this(preselectedImage) { - this._requestedVerb = verb; - this.ApplyRequestedVerb(); - this.CorrectArchiveOptimizeAmbiguity(); - } - - /// - /// Path of the image last successfully mutated by this window. Stays null while the window only - /// previews. - /// - public string? MutatedImagePath { get; private set; } - - /// - /// Raised once an operation has modified the image on disk, carrying the path that changed. Hosts - /// compare it against their loaded archive and reload if they match. - /// - public event Action? ArchiveMutated; - - private void NotifyMutated(string path) { - this.MutatedImagePath = path; - this.ArchiveMutated?.Invoke(path); - } - - // ── Construction ──────────────────────────────────────────────────────────────────────────── - - private void BuildImageBox() { - this._browse.Click += (_, _) => { - var dialog = new OpenFileDialog { - Title = "Select filesystem image or archive", - Filter = "Supported files|*.img;*.iso;*.d64;*.d71;*.d81;*.adf;*.dsk;*.po;*.atr;*.ssd;*.dsd;*.zip;*.7z;*.tar;*.lzh;*.arj" - + "|Filesystem images|*.img;*.iso;*.d64;*.d71;*.d81;*.adf;*.dsk;*.po;*.atr;*.ssd;*.dsd" - + "|Archives|*.zip;*.7z;*.tar;*.lzh;*.arj|All Files|*.*", - }; - if (dialog.ShowDialog() == DialogResult.OK) this.LoadImage(dialog.FileName); - }; - - this._imageBox.Controls.AddRange(this._imagePathBox, this._browse); - this.Controls.Add(this._imageBox); - } - - private void BuildInfoPanel() { - this._infoPanel.Controls.AddRange( - Caption("Format:", 0), this._formatLabel, - Caption("Defrag support:", 22), this._supportLabel, - Caption("Image size:", 44), this._sizeLabel); - this.Controls.Add(this._infoPanel); - - static Label Caption(string text, int y) => new() { - Bounds = new(8, y + 4, 120, 20), - Text = text, - Font = new(DefaultTheme.Instance.DefaultFont.Family, 9f, FontStyle.Bold), - }; - } - - private void BuildFsModes() { - var modes = new (RadioButton Button, string Detail)[] { - (this._modePackStart, "Move every file to the front of the image. Free space ends up after the last file. Closest match to a traditional defrag tool."), - (this._modePackEnd, "Move every file to the back of the image. Free space stays at the front. Useful before injecting a bootloader or installer payload at low offsets."), - (this._modeFillHoles, "Best-fit fill of existing free regions from tail extents. Doesn't guarantee contiguity but moves the minimum number of bytes. Use on huge images with a few small holes."), - (this._modeAscending, "Move only what reads backwards, so every file's blocks end up in ascending order and a sequential read never seeks back. A file left in three ascending pieces stays in three pieces — but this moves about a third of the bytes packing does, because it touches only what is out of order."), - (this._modeCarveHole, "Reserve a contiguous free region of the chosen size. Live data in the way is relocated to other free space, or appended."), - }; - - foreach (var (button, detail) in modes) { - this._toolTips.SetToolTip(button, detail); - button.CheckedChanged += (_, _) => this.OnModeChanged(); - this._fsModesGroup.Controls.Add(button); - } - - this._toolTips.SetToolTip(this._interleaveStride, - "Block interleave factor. 1 = contiguous (default), 2 = every-other-block, N = place each file's Kth block at (start + K*stride). Range 1-256."); - - this._metadataZone.Items.AddRange([ - "Don't move metadata", - "Metadata at front (fast access)", - "Metadata at back (data-first)", - "Metadata in middle (min seek)", - "Before file content (read-ahead)", - ]); - this._metadataZone.SelectedIndex = 0; - this._toolTips.SetToolTip(this._metadataZone, - "Controls where filesystem metadata and directory entries are placed during defragmentation."); - - this._layoutProfile.SelectedIndexChanged += (_, _) => this.OnLayoutProfileChanged(); - this._toolTips.SetToolTip(this._layoutProfile, - "Optional zone-based layout template. When set, files are placed into named byte ranges with per-zone sort orders. Overrides the layout strategy and metadata placement above."); - - this._editProfiles.Click += (_, _) => { - new LayoutProfileEditor().ShowDialog(this); - this.RefreshLayoutProfilesCombo(); - }; - this._toolTips.SetToolTip(this._editProfiles, "Open the layout profile editor to create, modify, or delete profiles."); - - this._fsModesGroup.Controls.AddRange( - new Label { Text = "Block interleave:" }, this._interleaveStride, - new Label { Text = "1 = contiguous, 2 = every-other-block, up to 256", ForeColor = Color.DimGray }, - new Label { Text = "Metadata placement:" }, this._metadataZone, - new Label { Text = "Layout profile:" }, this._layoutProfile, this._editProfiles); - - this.Controls.Add(this._fsModesGroup); - } - - private void BuildArchiveRepack() { - this._archiveRepackNote.Text = - "Archive repack: entries will be extracted and re-created with optimal compression settings. " - + "The layout strategy modes above do not apply to archive formats."; - this._toolTips.SetToolTip(this._smartSolidRepack, - "Tries 5 different file grouping strategies and picks the one that produces the smallest archive. More thorough but slower than a plain repack."); - - this._metadataPlacement.Items.AddRange(["Format default", "Metadata first", "Data first"]); - this._metadataPlacement.SelectedIndex = 0; - - this._archiveRepackGroup.Controls.AddRange( - this._archiveRepackNote, this._smartSolidRepack, this._metadataPlacementCaption, this._metadataPlacement); - this.Controls.Add(this._archiveRepackGroup); - } - - private void BuildCarveOptions() { - this._toolTips.SetToolTip(this._holeSize, "Size in bytes. Suffixes: k=KiB, m=MiB, g=GiB. Examples: 64m, 1g, 524288."); - this._toolTips.SetToolTip(this._holeAt, "Byte offset where the hole should start. 'auto' = at the end after the last live extent."); - - this._carveOptsGroup.Controls.AddRange( - new Label { Text = "Hole size:" }, this._holeSize, - new Label { Text = "(64m / 1g / 524288)", ForeColor = Color.DimGray }, - new Label { Text = "Hole offset:" }, this._holeAt, - new Label { Text = "(auto / decimal byte offset)", ForeColor = Color.DimGray }); - - this.Controls.Add(this._carveOptsGroup); - } - - private void BuildBlockMapArea() { - this._blockMapView.Items.AddRange(["Linear blocks", "Circular platter (2-D)", "Cylinder stack (3-D)"]); - this._blockMapView.SelectedIndex = 0; - this._blockMapView.SelectedIndexChanged += (_, _) => this._blockMap.ViewMode = this._blockMapView.SelectedIndex switch { - 1 => BlockMapView.CircularPlatter, - 2 => BlockMapView.CylinderStack, - _ => BlockMapView.LinearBlocks, - }; - - this._blockMap.TileClicked += this.OnBlockMapTileClicked; - this._toolTips.SetToolTip(this._filesPanelToggle, "Show or hide the file list side panel."); - this._filesPanelToggle.CheckedChanged += (_, _) => { - this._split.Panel2Collapsed = !this._filesPanelToggle.Checked; - this.LayoutChildren(); - }; - - this._blockMapGroup.Controls.AddRange( - this._layoutStatus, new Label { Text = "View:" }, this._blockMapView, - this._blockMap, this._legend, this._filesPanelToggle); - - AddColumn(this._filesGrid, "Name", static o => ((FileRow)o!).Name, 160); - AddColumn(this._filesGrid, "Size", static o => ((FileRow)o!).SizeDisplay, 80, (a, b) => ((FileRow)a!).Size.CompareTo(((FileRow)b!).Size)); - var fragments = AddColumn(this._filesGrid, "Frag", static o => ((FileRow)o!).FragmentsDisplay, 44); - fragments.Alignment = ContentAlignment.MiddleCenter; - fragments.TooltipSelector = static _ => "How many separate stretches of the container this file's data occupies. 1 is contiguous; higher is fragmented. An em dash means the format offers no layout to count from."; - AddColumn(this._filesGrid, "Method", static o => ((FileRow)o!).MethodDisplay, 80); - AddColumn(this._filesGrid, "Modified", static o => ((FileRow)o!).ModifiedDisplay, 120, (a, b) => Nullable.Compare(((FileRow)a!).Modified, ((FileRow)b!).Modified)); - AddColumn(this._filesGrid, "Class", static o => ((FileRow)o!).Class, 64); - - // The grid sorts itself; this only observes the result so the status line can report it. - this._filesGrid.CellClick += (_, _) => this.TrackSort(); - - this._filesPanelGroup.Controls.AddRange(this._filesPanelStatus, this._filesGrid); - - this._split.Panel1.Controls.Add(this._blockMapGroup); - this._split.Panel2.Controls.Add(this._filesPanelGroup); - this._blockMapGroup.Dock = DockStyle.Fill; - this._filesPanelGroup.Dock = DockStyle.Fill; - this._split.Panel2MinSize = 220; - this.Controls.Add(this._split); - } - - private static DataGridViewColumn AddColumn(DataGridView grid, string header, Func selector, int width, Comparison? sort = null) { - var column = new DataGridViewColumn(header, selector) { Width = width }; - if (sort is not null) column.SortComparison = sort; - grid.Columns.Add(column); - return column; - } - - private void BuildOutput() { - this._outputGroup.Controls.Add(this._outputBox); - this.Controls.AddRange(this._outputGroup, this._progress); - } - - private void BuildActions() { - this._toolTips.SetToolTip(this._scrambleSeed, "Seeds Scramble's shuffle. The same seed deals the same layout every run."); - this._toolTips.SetToolTip(this._scramble, "Scatter every block of every file across the volume — fragmentation on purpose, so Defragment has something real to work against. Content is preserved; only the layout changes. Asks first."); - this._toolTips.SetToolTip(this._cancel, "Cancel the active maintenance pass. Staged rebuilds are discarded and leave the original unchanged; native in-place moves cancel at a safe boundary when supported."); - this._toolTips.SetToolTip(this._minimalGeometry, "Compact rebuilds at the smallest geometry the format allows (e.g. a 1.44 MB FAT floppy → a few KB). Contents preserved, but the result may not be a standard mountable image."); - this._toolTips.SetToolTip(this._compact, "Defragment + optimize + shrink in one pass — the smallest valid container that still holds the same contents. Tick 'Minimal geometry' for the bare-minimum (non-standard) rebuild."); - this._toolTips.SetToolTip(this._purge, "Erase ALL live data, leaving a valid empty container. Distinct from Wipe Empty (which only zeros dead space)."); - this._toolTips.SetToolTip(this._wipeEmpty, "Zero-fill all unused space: free clusters, cluster-tip slack, dead bytes. Forensic cleanliness tool."); - this._toolTips.SetToolTip(this._shrink, "Defragment + truncate trailing free space to minimize image size."); - - this._run.Click += (_, _) => this.OnRunWithBlockProgress(); - this._cancel.Click += (_, _) => this.RequestMaintenanceCancellation(confirmNativeInPlace: true); - this._scramble.Click += (_, _) => this.OnScramble(); - this._compact.Click += (_, _) => this.OnCompact(); - this._purge.Click += (_, _) => this.OnPurge(); - this._wipeEmpty.Click += (_, _) => this.OnWipeEmpty(); - this._shrink.Click += (_, _) => this.OnShrink(); - this._close.Click += (_, _) => this.Close(); - - this.Controls.AddRange( - new Label { Text = "Seed" }, this._scrambleSeed, this._scramble, this._run, this._cancel, this._close, - this._minimalGeometry, this._compact, this._purge, this._wipeEmpty, this._shrink); - this.AcceptButton = this._run; - } - - // ── Layout ────────────────────────────────────────────────────────────────────────────────── - - private void LayoutChildren() { - const int Gutter = 12; - var width = this.ClientSize.Width - 2 * Gutter; - - this._imageBox.Bounds = new(Gutter, Gutter, width, 52); - this._imagePathBox.Bounds = new(8, 20, Math.Max(80, width - 110), 24); - this._browse.Bounds = new(width - 96, 20, 88, 24); - - var y = Gutter + 60; - this._infoPanel.Bounds = new(Gutter, y, width, 70); - this._formatLabel.Bounds = new(132, 4, width - 140, 20); - this._supportLabel.Bounds = new(132, 26, width - 140, 20); - this._sizeLabel.Bounds = new(132, 48, width - 140, 20); - - y += 78; - var strategyHeight = this._fsModesGroup.Visible ? 240 : 0; - this._fsModesGroup.Bounds = new(Gutter, y, width, 240); - this.LayoutFsModes(width); - - this._archiveRepackGroup.Bounds = new(Gutter, y, width, 110); - this._archiveRepackNote.Bounds = new(8, 20, width - 16, 34); - this._smartSolidRepack.Bounds = new(8, 58, 300, 20); - this._metadataPlacementCaption.Bounds = new(8, 82, 120, 20); - this._metadataPlacement.Bounds = new(132, 80, 180, 24); - if (this._archiveRepackGroup.Visible) strategyHeight = 110; - - y += strategyHeight + (strategyHeight > 0 ? 8 : 0); - if (this._carveOptsGroup.Visible) { - this._carveOptsGroup.Bounds = new(Gutter, y, width, 76); - LayoutCarveRow(this._carveOptsGroup, 0, 20, width); - LayoutCarveRow(this._carveOptsGroup, 3, 46, width); - y += 84; - } - - var actionsHeight = 76; - var outputHeight = 84; - var mapHeight = Math.Max(160, this.ClientSize.Height - y - outputHeight - actionsHeight - Gutter); - this._split.Bounds = new(Gutter, y, width, mapHeight); - if (!this._split.Panel2Collapsed) this._split.SplitterDistance = Math.Max(240, width * 2 / 3); - - this._layoutStatus.Bounds = new(6, 18, Math.Max(50, this._blockMapGroup.Width - 12), 18); - this._blockMapGroup.Controls[1].Bounds = new(6, 40, 36, 20); // "View:" - this._blockMapView.Bounds = new(44, 38, 200, 24); - this._legend.Bounds = new(6, this._blockMapGroup.Height - 46, Math.Max(50, this._blockMapGroup.Width - 130), 40); - this._filesPanelToggle.Bounds = new(this._blockMapGroup.Width - 120, this._blockMapGroup.Height - 36, 110, 22); - this._blockMap.Bounds = new(6, 66, Math.Max(50, this._blockMapGroup.Width - 12), Math.Max(60, this._blockMapGroup.Height - 66 - 50)); - - this._filesPanelStatus.Bounds = new(4, 18, Math.Max(50, this._filesPanelGroup.Width - 8), 18); - this._filesGrid.Bounds = new(4, 40, Math.Max(50, this._filesPanelGroup.Width - 8), Math.Max(40, this._filesPanelGroup.Height - 46)); - - y += mapHeight + 8; - this._outputGroup.Bounds = new(Gutter, y, width, outputHeight - 8); - this._outputBox.Bounds = new(6, 18, Math.Max(50, width - 12), outputHeight - 32); - this._progress.Bounds = new(Gutter, y + outputHeight - 12, width, 4); - - this.LayoutActionButtons(Gutter, this.ClientSize.Height - actionsHeight + 8, width); - - static void LayoutCarveRow(GroupBox box, int first, int top, int width) { - box.Controls[first].Bounds = new(8, top + 2, 110, 20); - box.Controls[first + 1].Bounds = new(122, top, Math.Max(60, width - 260), 22); - box.Controls[first + 2].Bounds = new(width - 130, top + 2, 124, 20); - } - } - - private void LayoutFsModes(int width) { - var controls = this._fsModesGroup.Controls; - var y = 20; - - foreach (var button in new[] { this._modePackStart, this._modePackEnd, this._modeFillHoles, this._modeAscending, this._modeCarveHole }) { - button.Bounds = new(8, y, Math.Max(100, width - 20), 20); - y += 24; - } - - y += 6; - controls[5].Bounds = new(8, y + 2, 118, 20); // "Block interleave:" - this._interleaveStride.Bounds = new(128, y, 76, 22); - controls[7].Bounds = new(212, y + 2, 300, 20); // hint - - y += 28; - controls[8].Bounds = new(8, y + 2, 118, 20); // "Metadata placement:" - this._metadataZone.Bounds = new(128, y, 200, 24); - - y += 30; - controls[10].Bounds = new(8, y + 2, 118, 20); // "Layout profile:" - this._layoutProfile.Bounds = new(128, y, Math.Max(120, width - 280), 24); - this._editProfiles.Bounds = new(width - 140, y, 120, 24); - } - - private void LayoutActionButtons(int gutter, int top, int width) { - // Second row first, so the primary actions sit closest to the bottom edge. - var right = gutter + width; - var x = right - 108; - this._shrink.Bounds = new(x, top, 100, 28); - x -= 108; - this._wipeEmpty.Bounds = new(x, top, 100, 28); - x -= 108; - this._purge.Bounds = new(x, top, 100, 28); - x -= 108; - this._compact.Bounds = new(x, top, 100, 28); - x -= 140; - this._minimalGeometry.Bounds = new(x, top + 4, 134, 20); - - var second = top + 34; - x = right - 88; - this._close.Bounds = new(x, second, 80, 28); - x -= 88; - this._cancel.Bounds = new(x, second, 80, 28); - x -= 128; - this._run.Bounds = new(x, second, 120, 28); - x -= 108; - this._scramble.Bounds = new(x, second, 100, 28); - x -= 68; - this._scrambleSeed.Bounds = new(x, second + 3, 60, 22); - this.Controls[^11].Bounds = new(x - 40, second + 5, 36, 20); // "Seed" - } - - // ── Layout profiles ───────────────────────────────────────────────────────────────────────── - - private void RefreshLayoutProfilesCombo() { - var previousPath = this._layoutProfile.SelectedIndex >= 0 && this._layoutProfile.SelectedIndex < this._layoutProfileEntries.Count - ? this._layoutProfileEntries[this._layoutProfile.SelectedIndex]?.FilePath - : null; - - this._layoutProfile.Items.Clear(); - this._layoutProfileEntries.Clear(); - - this._layoutProfile.Items.Add("(none)"); - this._layoutProfileEntries.Add(null); - foreach (var entry in LayoutProfileStore.List()) { - this._layoutProfile.Items.Add($"{entry.Name} [{(entry.Origin == ProfileOrigin.Builtin ? "Built-in" : "User")}]"); - this._layoutProfileEntries.Add(entry); - } - - // Preserve the user's previous pick across refreshes. - if (!string.IsNullOrEmpty(previousPath)) - for (var i = 0; i < this._layoutProfileEntries.Count; ++i) - if (string.Equals(this._layoutProfileEntries[i]?.FilePath, previousPath, StringComparison.OrdinalIgnoreCase)) { - this._layoutProfile.SelectedIndex = i; - return; - } - - this._layoutProfile.SelectedIndex = 0; - } - - private void OnLayoutProfileChanged() { - var index = this._layoutProfile.SelectedIndex; - if (index < 0 || index >= this._layoutProfileEntries.Count || this._layoutProfileEntries[index] is not { } entry) { - this._selectedLayoutProfile = null; - return; - } - - try { - this._selectedLayoutProfile = LayoutProfileStore.Load(entry); - } catch (Exception ex) { - MessageBox.Show(this, $"Failed to load layout profile '{entry.Name}':\n{ex.Message}", - "Layout profile", MessageBoxButtons.OK, MessageBoxIcon.Error); - this._selectedLayoutProfile = null; - this._layoutProfile.SelectedIndex = 0; - } - } - - // ── Image loading ─────────────────────────────────────────────────────────────────────────── - - private void ApplyRequestedVerb() { - if (this._requestedVerb is not { } verb) return; - - this.Text = verb switch { - MaintenanceVerb.Optimize => "Optimize", - MaintenanceVerb.Shrink => "Shrink", - MaintenanceVerb.Defragment => "Defragment", - MaintenanceVerb.Purge => "Purge", - MaintenanceVerb.WipeEmpty => "Wipe Empty", - MaintenanceVerb.Compact => "Compact", - MaintenanceVerb.Scramble => "Scramble", - _ => "Maintenance", - }; - - var target = verb switch { - MaintenanceVerb.Shrink => this._shrink, - MaintenanceVerb.Purge => this._purge, - MaintenanceVerb.WipeEmpty => this._wipeEmpty, - MaintenanceVerb.Compact => this._compact, - MaintenanceVerb.Scramble => this._scramble, - // Optimize and Defragment both live on the morphing Run button. - _ => this._run, - }; - - if (!target.Enabled) return; - this.AcceptButton = target; - target.Focus(); - } - - /// - /// The loader sees before archive-repack support. When the - /// caller explicitly asked to optimize, correct that ambiguity so ZIP and 7z expose their repack - /// controls instead of looking like filesystem-only defraggers. - /// - private void CorrectArchiveOptimizeAmbiguity() { - if (this._requestedVerb != MaintenanceVerb.Optimize || this._formatId is not { Length: > 0 } id) return; - - var descriptor = FormatRegistry.GetById(id); - var ops = FormatRegistry.GetArchiveOps(id); - if (descriptor?.Category is not (FormatCategory.Archive or FormatCategory.CompoundTar) || ops is not IArchiveCreatable) return; - - this._isArchiveMode = true; - this._archiveOps = ops; - this._isSevenZipFormat = string.Equals(id, "SevenZip", StringComparison.Ordinal); - this._fsModesGroup.Visible = false; - this._archiveRepackGroup.Visible = true; - this._smartSolidRepack.Visible = this._isSevenZipFormat; - this._run.Text = "Optimize"; - this._run.Enabled = true; - this._supportLabel.Text = "Archive re-layout/repack with live staged-target visualization."; - this._supportLabel.ForeColor = Color.DarkGreen; - this._layoutStatus.Text = "Source + staged-target address spaces share the chart for progress; offsets are projected, not physical equivalence."; - this.LayoutChildren(); - } - - private void LoadImage(string path) { - this._imagePath = path; - this._isArchiveMode = false; - this._isFileInternalMode = false; - this._isSevenZipFormat = false; - this._archiveOps = null; - this._chunkMover = null; - - this._imagePathBox.Text = path; - this._imagePathBox.ForeColor = Color.Black; - - FormatRegistration.EnsureInitialized(); - var format = FormatDetector.Detect(path); - this._formatLabel.Text = format.ToString(); - this._formatId = format.ToString(); - - var ops = FormatRegistry.GetArchiveOps(this._formatId); - this._defragmentable = ops as IArchiveDefragmentable; - - var isArchiveLayout = ops is IArchiveLayoutMap; - var isArchiveCreatable = ops is IArchiveCreatable; - var isFileInternalLayout = ops is IFileInternalLayoutMap; - var isFileInternalOptimizable = ops is IFileInternalChunkMover; - - if (this._defragmentable is not null) { - var supported = ProbeSupportedModes(this._defragmentable); - this._supportLabel.Text = supported.Count >= 4 - ? "All four modes (in-place defragment)." - : $"{string.Join(", ", supported)} (other modes throw NotSupported)"; - this._supportLabel.ForeColor = Color.DarkGreen; - this._run.Text = "Defragment"; - this._run.Enabled = true; - } else if (isFileInternalLayout) { - this._isFileInternalMode = true; - this._archiveOps = ops; - this._chunkMover = ops as IFileInternalChunkMover; - this._supportLabel.Text = isFileInternalOptimizable - ? "File-internal layout optimization (e.g. MP4 fast-start)." - : "File-internal layout viewable but optimization not supported (read-only)."; - this._supportLabel.ForeColor = isFileInternalOptimizable ? Color.DarkGreen : Color.DarkOrange; - this._run.Enabled = isFileInternalOptimizable; - this._run.Text = "Optimize"; - } else if (isArchiveLayout || isArchiveCreatable) { - this._isArchiveMode = true; - this._isSevenZipFormat = this._formatId == "SevenZip"; - this._archiveOps = ops; - this._supportLabel.Text = isArchiveCreatable - ? "Archive optimization (extract + repack with optimal settings)." - : "Archive layout viewable but format does not support creation (read-only)."; - this._supportLabel.ForeColor = isArchiveCreatable ? Color.DarkGreen : Color.DarkOrange; - this._run.Enabled = isArchiveCreatable; - this._run.Text = "Optimize"; - } else { - this._supportLabel.Text = "Not supported by this format."; - this._supportLabel.ForeColor = Color.OrangeRed; - this._run.Text = "Defragment"; - this._run.Enabled = false; - } - - var showFsModes = !this._isArchiveMode && !this._isFileInternalMode; - this._fsModesGroup.Visible = showFsModes; - this._archiveRepackGroup.Visible = !showFsModes; - this._smartSolidRepack.Visible = this._isSevenZipFormat; - this._metadataPlacement.Visible = this._isFileInternalMode; - this._metadataPlacementCaption.Visible = this._isFileInternalMode; - - this._shrink.Enabled = this._formatId is "Fat" or "Ext" or "Ext1" or "Vhd" || ops is IArchiveShrinkable; - this._wipeEmpty.Enabled = ops is IWipeEmpty or IFilesystemExtentMap or IArchiveLayoutMap; - // Purge is realised through the modifier's Remove over every entry. - this._purge.Enabled = ops is IArchiveModifiable; - this._compact.Enabled = ops is IArchiveDefragmentable or IArchiveShrinkable or IArchiveCreatable; - this._minimalGeometry.Enabled = ops is IArchiveCreatable and IFormatOptionsSchema; - - // Scramble has no fallback: a rebuild would pack the volume, the opposite of what it says. - var canScramble = ops is IFilesystemScrambleable; - this._scramble.Enabled = canScramble; - this._scrambleSeed.Enabled = canScramble; - - var info = new FileInfo(path); - this._sizeLabel.Text = $"{FormatSize(info.Length)} ({info.Length:N0} bytes)"; - - this.PreviewBlockMap(path, ops); - this.ApplyRequestedVerb(); - this.LayoutChildren(); - } - - // ── Block map preview ─────────────────────────────────────────────────────────────────────── - - /// - /// Builds the initial block-map snapshot. A descriptor that exposes a real extent, archive or - /// chunk layout gets the honest picture; anything else falls back to sizes laid end to end from - /// zero, which the status line labels as approximate. - /// - private void PreviewBlockMap(string path, IArchiveFormatOperations? ops, bool wasMutated = false) { - if (wasMutated) this.NotifyMutated(path); - - this._blockMap.BlockMap = null; - this._blockMap.ImageSize = 0; - this._filesGrid.DataSource = Array.Empty(); - this._fileRowCount = 0; - this._filesSortDescription = "listing order"; - this._filesPanelStatus.Text = "—"; - this._layoutStatus.Text = "—"; - if (ops is null) return; - - if (ops is IFilesystemExtentMap extentMap) - try { - using var stream = File.OpenRead(path); - if (this.TryRenderExtentMap(extentMap, stream, ops)) return; - } catch { - // Extent walker failed mid-stream; fall through so the user still sees something. - } - - if (ops is IArchiveLayoutMap archiveLayout) - try { - using var stream = File.OpenRead(path); - if (this.TryRenderArchiveLayout(archiveLayout, stream, ops)) return; - } catch { - // Layout walker failed; fall through to the approximation. - } - - if (ops is IFileInternalLayoutMap fileLayout) - try { - using var stream = File.OpenRead(path); - if (this.TryRenderFileInternalLayout(fileLayout, stream)) return; - } catch { - // Chunk walker failed; fall through to the approximation. - } - - this._layoutStatus.Text = "Approximate layout (post-defrag preview)"; - try { - using var stream = File.OpenRead(path); - var entries = ops.List(stream, password: null); - var imageSize = stream.Length; - var map = new List(); - var rows = new List(); - var offset = 0L; - - var files = entries.Where(e => !e.IsDirectory).ToList(); - var thresholds = ComputeMtimeQuartiles(files.Select(f => f.LastModified)); - - for (var k = 0; k < files.Count; ++k) { - var e = files[k]; - var size = Math.Max(1, e.OriginalSize); - var cls = Classify(e.LastModified, thresholds, k, files.Count); - - map.Add(new(offset, size, DefragBlockKind.Used, e.Name, cls)); - rows.Add(new() { - Name = e.Name, - Size = e.OriginalSize, - SizeDisplay = FormatSize(e.OriginalSize), - // The format reported no layout, so these tiles are sizes laid end to end rather than - // where anything actually is. Counting runs off that would report one for everything. - FragmentsDisplay = "—", - Modified = e.LastModified, - ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "—", - Class = cls.ToString(), - }); - - offset += size; - if (offset >= imageSize) break; - } - - if (offset < imageSize) map.Add(new(offset, imageSize - offset, DefragBlockKind.Free)); - - this._blockMap.BlockMap = map; - this._blockMap.ImageSize = imageSize; - this.SetFileRows(rows); - } catch { - // Preview is best-effort; an empty map is an acceptable outcome. - } - } - - /// - /// Splits the modification times into four equal-count buckets. The most recent quarter is hot, - /// the oldest is frozen. - /// - private static (DateTime? Cold, DateTime? Normal, DateTime? Hot)? ComputeMtimeQuartiles(IEnumerable times) { - var sorted = times.Where(t => t.HasValue).Select(t => t!.Value).Order().ToList(); - if (sorted.Count == 0) return null; - - var n = sorted.Count; - return (sorted[Math.Min(n - 1, n / 4)], sorted[Math.Min(n - 1, n / 2)], sorted[Math.Min(n - 1, 3 * n / 4)]); - } - - private static DefragBlockClass Classify(DateTime? modified, (DateTime? Cold, DateTime? Normal, DateTime? Hot)? thresholds, int index, int count) { - if (thresholds is not { } t) - // No dates anywhere — fall back to listing-order quartile so the chart still has structure. - return (index * 4 / Math.Max(1, count)) switch { - 0 => DefragBlockClass.Hot, - 1 => DefragBlockClass.Normal, - 2 => DefragBlockClass.Cold, - _ => DefragBlockClass.Frozen, - }; - - // An undated entry in a dated set stands out as frozen rather than being guessed at. - if (modified is not { } when_) return DefragBlockClass.Frozen; - - if (when_ < t.Cold) return DefragBlockClass.Frozen; - if (when_ < t.Normal) return DefragBlockClass.Cold; - return when_ < t.Hot ? DefragBlockClass.Normal : DefragBlockClass.Hot; - } - - private bool TryRenderExtentMap(IFilesystemExtentMap extentMap, Stream stream, IArchiveFormatOperations ops) { - var imageSize = stream.Length; - if (imageSize <= 0) return false; - - var filled = NormalizeExtents(extentMap.EnumerateExtents(stream).ToList(), imageSize); - if (filled is null) return false; - - Dictionary? mtimeByName = null; - List? entries = null; - try { - stream.Position = 0; - entries = ops.List(stream, password: null); - mtimeByName = entries.Where(e => !e.IsDirectory) - .GroupBy(e => e.Name) - .ToDictionary(g => g.Key, g => g.First().LastModified); - } catch { - // Classification is best-effort; the layout itself is what matters here. - } - - var thresholds = mtimeByName is null ? null : ComputeMtimeQuartiles(mtimeByName.Values); - var classified = new List(filled.Count); - foreach (var ex in filled) { - if (ex.Kind != DefragBlockKind.Used || ex.FileName is null || mtimeByName is null - || !mtimeByName.TryGetValue(ex.FileName, out var mtime)) { - classified.Add(ex); - continue; - } - - classified.Add(ex with { Classification = thresholds is null ? DefragBlockClass.Normal : Classify(mtime, thresholds, 0, 1) }); - } - - this._blockMap.BlockMap = classified.Count > MaxExtents ? BinExtents(classified, MaxExtents) : classified; - this._blockMap.ImageSize = imageSize; - - var fragCount = CountRunsByOwner(classified); - if (entries is not null) - this.SetFileRows([.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { - Name = e.Name, - Size = e.OriginalSize, - SizeDisplay = FormatSize(e.OriginalSize), - FragmentsDisplay = fragCount.TryGetValue(e.Name, out var runs) && runs > 0 ? runs.ToString("N0") : "—", - Modified = e.LastModified, - ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "—", - Class = "—", - })]); - - var usedExtents = classified.Count(ex => ex.Kind == DefragBlockKind.Used); - var fragmentedFiles = entries is null ? 0 : fragCount.Values.Count(runs => runs > 1); - this._layoutStatus.Text = fragmentedFiles > 0 - ? $"Real on-disk layout — {usedExtents:N0} extents, {fragmentedFiles:N0} fragmented file(s)" - : $"Real on-disk layout — {usedExtents:N0} extents (no fragmentation detected)"; - return true; - } - - private bool TryRenderArchiveLayout(IArchiveLayoutMap archiveLayout, Stream stream, IArchiveFormatOperations? ops) { - var imageSize = stream.Length; - if (imageSize <= 0) return false; - - var filled = NormalizeExtents(archiveLayout.EnumerateLayout(stream).ToList(), imageSize); - if (filled is null) return false; - - Dictionary? methodByName = null; - List? entries = null; - try { - stream.Position = 0; - entries = ops?.List(stream, password: null); - if (entries is not null) - methodByName = entries.Where(e => !e.IsDirectory) - .GroupBy(e => e.Name) - .ToDictionary(g => g.Key, g => g.First().Method ?? ""); - } catch { - // Classification is best-effort. - } - - var classified = new List(filled.Count); - foreach (var ex in filled) { - if (ex.Kind != DefragBlockKind.Used || ex.FileName is null || methodByName is null - || !methodByName.TryGetValue(ex.FileName, out var method)) { - classified.Add(ex); - continue; - } - - classified.Add(ex with { Classification = ClassifyByMethod(method) }); - } - - this._blockMap.BlockMap = classified.Count > MaxExtents ? BinExtents(classified, MaxExtents) : classified; - this._blockMap.ImageSize = imageSize; - - if (entries is not null) { - // Counted off the layout the container reported. An entry the map does not name is not one - // run — it is unmeasured. - var runsByName = CountRunsByOwner(classified); - this.SetFileRows([.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { - Name = e.Name, - Size = e.OriginalSize, - SizeDisplay = FormatSize(e.OriginalSize), - FragmentsDisplay = runsByName.TryGetValue(e.Name, out var runs) ? runs.ToString("N0") : "—", - MethodDisplay = e.Method ?? "", - Modified = e.LastModified, - ModifiedDisplay = e.LastModified is { } dt ? dt.ToString("yyyy-MM-dd HH:mm") : "", - Class = ClassifyByMethod(e.Method ?? "").ToString(), - })]); - } - - var regionCount = classified.Count(ex => ex.Kind == DefragBlockKind.Used); - var freeBytes = classified.Where(ex => ex.Kind == DefragBlockKind.Free).Sum(ex => ex.Length); - this._layoutStatus.Text = freeBytes > 0 - ? $"Real archive layout — {regionCount:N0} regions, {FormatSize(freeBytes)} wasted" - : $"Real archive layout — {regionCount:N0} regions (tightly packed)"; - return true; - } - - private bool TryRenderFileInternalLayout(IFileInternalLayoutMap fileLayout, Stream stream) { - var imageSize = stream.Length; - if (imageSize <= 0) return false; - - var filled = NormalizeExtents(fileLayout.EnumerateChunks(stream).ToList(), imageSize); - if (filled is null) return false; - - this._blockMap.BlockMap = filled; - this._blockMap.ImageSize = imageSize; - - // A row here is one stretch of the file, so the count belongs to the chunk's owner and comes - // from the same layout the map was drawn from. - var chunkRuns = CountRunsByOwner(filled, onlyKind: null); - this.SetFileRows([.. filled.Select(ch => new FileRow { - Name = ch.FileName ?? ch.Kind.ToString(), - Size = ch.Length, - SizeDisplay = FormatSize(ch.Length), - FragmentsDisplay = ch.FileName is { } owner && chunkRuns.TryGetValue(owner, out var runs) ? runs.ToString("N0") : "—", - MethodDisplay = ch.Kind.ToString(), - Class = ch.Classification?.ToString() ?? "—", - })]); - - this._layoutStatus.Text = $"File-internal layout — {filled.Count(c => c.Kind != DefragBlockKind.Free):N0} chunks"; - return true; - } - - /// - /// Sorts extents by offset, clips them to the image, fills the gaps as free space and trims - /// overlaps so a later extent at the same offset wins. Returns null when nothing usable remains. - /// - private static List? NormalizeExtents(List raw, long imageSize) { - if (raw.Count == 0) return null; - raw.Sort(static (a, b) => a.Offset.CompareTo(b.Offset)); - - var clipped = new List(raw.Count); - foreach (var ex in raw) { - if (ex.Offset >= imageSize) continue; - var length = Math.Min(ex.Length, imageSize - ex.Offset); - if (length <= 0) continue; - clipped.Add(ex with { Length = length }); - } - if (clipped.Count == 0) return null; - - var filled = new List(clipped.Count * 2); - var cursor = 0L; - foreach (var ex in clipped) { - if (ex.Offset > cursor) - filled.Add(new(cursor, ex.Offset - cursor, DefragBlockKind.Free)); - else if (ex.Offset < cursor) { - if (ex.Offset + ex.Length <= cursor) continue; - var trimmed = ex with { Length = ex.Length - (cursor - ex.Offset), Offset = cursor }; - filled.Add(trimmed); - cursor = trimmed.Offset + trimmed.Length; - continue; - } - - filled.Add(ex); - cursor = ex.Offset + ex.Length; - } - - if (cursor < imageSize) filled.Add(new(cursor, imageSize - cursor, DefragBlockKind.Free)); - return filled; - } - - /// Maps a compression method name to a thermal class for colour-coding archive entries. - private static DefragBlockClass ClassifyByMethod(string method) { - var m = method.ToUpperInvariant(); - if (m.Contains("STORE") || m.Contains("COPY") || m is "NONE" or "") return DefragBlockClass.Frozen; - if (m.Contains("LZMA") || m.Contains("PPMD") || m.Contains("BZIP")) return DefragBlockClass.Hot; - if (m.Contains("ZSTD")) return DefragBlockClass.Cold; - return DefragBlockClass.Normal; - } - - /// - /// Bins an oversized extent list down to by merging adjacent - /// same-kind extents, then — if still over budget — by equal-sized byte bins taking each bin's - /// dominant kind. - /// - private static List BinExtents(List input, int maxCount) { - if (input.Count <= maxCount) return input; - - var merged = new List(input.Count); - foreach (var ex in input) { - if (merged.Count == 0) { - merged.Add(ex); - continue; - } - - var last = merged[^1]; - if (last.Kind == ex.Kind && last.Offset + last.Length == ex.Offset && last.FileName == ex.FileName) - merged[^1] = last with { Length = last.Length + ex.Length }; - else - merged.Add(ex); - } - if (merged.Count <= maxCount) return merged; - - var imageSize = merged[^1].Offset + merged[^1].Length; - var bytesPerBin = (double)imageSize / maxCount; - var binned = new List(maxCount); - var binStart = 0L; - var binIndex = 0; - var binKind = DefragBlockKind.Free; - var binBestLength = 0L; - string? binBestName = null; - - foreach (var ex in merged) { - while (ex.Offset + ex.Length > binStart + bytesPerBin && binIndex < maxCount - 1) { - if (ex.Length > binBestLength) { - binBestLength = ex.Length; - binKind = ex.Kind; - binBestName = ex.FileName; - } - - binned.Add(new(binStart, (long)bytesPerBin, binKind, binBestName)); - binStart += (long)bytesPerBin; - ++binIndex; - binKind = DefragBlockKind.Free; - binBestLength = 0; - binBestName = null; - } - - if (ex.Length > binBestLength) { - binBestLength = ex.Length; - binKind = ex.Kind; - binBestName = ex.FileName; - } - } - - binned.Add(new(binStart, imageSize - binStart, binKind, binBestName)); - return binned; - } - - /// - /// How many separate stretches of the container each owner's data occupies. - /// - /// Counting the entries a map yields would measure the map rather than the layout: some merge an - /// owner's consecutive blocks into one entry and some emit one per block, so a contiguous file and - /// a scattered one would report the same figure. Stretches that end exactly where the next begins - /// are one run either way. - /// - /// - private static Dictionary CountRunsByOwner( - IEnumerable extents, DefragBlockKind? onlyKind = DefragBlockKind.Used) { - var runs = new Dictionary(StringComparer.Ordinal); - var endOf = new Dictionary(StringComparer.Ordinal); - - foreach (var extent in extents) { - if (onlyKind is { } wanted && extent.Kind != wanted) continue; - if (extent.FileName is not { } owner) continue; - - if (!endOf.TryGetValue(owner, out var previousEnd) || previousEnd != extent.Offset) { - runs.TryGetValue(owner, out var count); - runs[owner] = count + 1; - } - - endOf[owner] = extent.Offset + extent.Length; - } - - return runs; - } - - private void SetFileRows(List rows) { - this._filesGrid.DataSource = rows; - this._fileRowCount = rows.Count; - this._filesSortDescription = "listing order"; - this.UpdateFilesPanelStatus(); - } - - /// - /// Refreshes the row-count and ordering line above the file grid, so a very large filesystem still - /// gives a sense of scale that scrolling alone does not. - /// - private void UpdateFilesPanelStatus() { - if (this._fileRowCount <= 0) { - this._filesPanelStatus.Text = "—"; - return; - } - - var countLabel = this._fileRowCount == 1 ? "1 file" : $"{this._fileRowCount:N0} files"; - this._filesPanelStatus.Text = $"{countLabel} · sorted by {this._filesSortDescription}"; - } - - private void TrackSort() { - if (this._filesGrid.SortedColumn is not { } column) return; - var arrow = this._filesGrid.SortOrder == SortOrder.Ascending ? "↑" : "↓"; - this._filesSortDescription = $"{column.HeaderText} {arrow}"; - this.UpdateFilesPanelStatus(); - } - - /// - /// Opens a modeless drill-down listing every block intersecting the clicked tile. Modeless so the - /// user can keep clicking tiles to compare; each click re-targets the same window. - /// - private void OnBlockMapTileClicked(object? sender, TileClickedEventArgs e) { - this._tileContentsWindow ??= CreateTileWindow(this); - this._tileContentsWindow.SetContents(e.StartOffset, e.EndOffset, e.Contents); - this._tileContentsWindow.Show(); - - TileContentsWindow CreateTileWindow(Form owner) { - var window = new TileContentsWindow(); - window.FormClosed += (_, _) => this._tileContentsWindow = null; - return window; - } - } - - private void OnModeChanged() { - this._carveOptsGroup.Visible = this._modeCarveHole.Checked; - this.LayoutChildren(); - } - - // ── Operation plumbing ────────────────────────────────────────────────────────────────────── - - private void OnFormClosing(object? sender, FormClosingEventArgs e) { - if (e.Cancel || this._maintenanceCancellation is null) return; - e.Cancel = true; - this.RequestMaintenanceCancellation(confirmNativeInPlace: true); - } - - private CancellationToken BeginMaintenanceOperation(string name, bool staged) { - this._maintenanceCancellation?.Dispose(); - this._maintenanceCancellation = new(); - this._maintenanceOperationName = name; - this._maintenanceIsStaged = staged; - this._maintenanceCommitStarted = false; - - this._cancel.Enabled = true; - foreach (var button in new[] { this._browse, this._run, this._shrink, this._wipeEmpty, this._purge, this._compact }) - button.Enabled = false; - - if (staged) - this._layoutStatus.Text = $"{name}: building a staged target — original remains unchanged until commit."; - - return this._maintenanceCancellation.Token; - } - - private void EndMaintenanceOperation() { - this._maintenanceCancellation?.Dispose(); - this._maintenanceCancellation = null; - this._maintenanceOperationName = null; - this._maintenanceIsStaged = false; - this._maintenanceCommitStarted = false; - this._cancel.Enabled = false; - this._browse.Enabled = true; - - // Reloading restores every button to what the format supports, rather than to whatever the last - // operation happened to leave behind. - if (this._imagePath is { Length: > 0 } path && File.Exists(path)) this.LoadImage(path); - } - - private void RequestMaintenanceCancellation(bool confirmNativeInPlace) { - if (this._maintenanceCancellation is not { } cts || cts.IsCancellationRequested) return; - - if (this._maintenanceCommitStarted) { - this.Append("Cancellation ignored: verified target commit has started; completing it is safer than interrupting the only commit step."); - this._cancel.Enabled = false; - return; - } - - if (!this._maintenanceIsStaged && confirmNativeInPlace) { - var answer = MessageBox.Show(this, - $"Cancel {this._maintenanceOperationName ?? "maintenance"}?\n\n" - + "This operation is moving data in place. Moves already completed are not rolled back. " - + "Cancellation is best-effort at the next safe boundary, so the layout may be partially changed although the container should remain valid.", - "Cancel in-place maintenance", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (answer != DialogResult.Yes) return; - this.Append("Cancellation requested — already-completed in-place moves will remain moved."); - } else { - this.Append("Cancellation requested — staged target will be discarded; existing archive remains unchanged."); - } - - this._cancel.Enabled = false; - cts.Cancel(); - } - - private void OnRunWithBlockProgress() { - if (this._imagePath is null || this._maintenanceCancellation is not null) return; - - if (this._isFileInternalMode) { - this.OnRunFileInternalOptimize(); - return; - } - - var ops = this._formatId is { Length: > 0 } id ? FormatRegistry.GetArchiveOps(id) : this._archiveOps; - var descriptor = this._formatId is { Length: > 0 } formatId ? FormatRegistry.GetById(formatId) : null; - var explicitlyOptimizingArchive = this._requestedVerb == MaintenanceVerb.Optimize - && descriptor?.Category is FormatCategory.Archive or FormatCategory.CompoundTar - && ops is IArchiveCreatable; - - if (this._isArchiveMode || explicitlyOptimizingArchive) { - this.RunArchiveOptimizeWithBlockProgress(ops); - return; - } - - this.RunDefragWithBlockProgress(); - } - - private void RunDefragWithBlockProgress() { - if (this._imagePath is not { } path || this._defragmentable is not { } defragmentable) return; - - var mode = this.SelectedMode(); - var holeSize = mode == DefragMode.CarveHole ? ParseSize(this._holeSize.Text) : 0L; - var holeAt = mode == DefragMode.CarveHole ? ParseHoleAt(this._holeAt.Text) : -1L; - var interleaveStride = ParseInterleaveStride(this._interleaveStride.Text); - var metadataZone = this.SelectedMetadataZone(); - var layoutProfile = this._selectedLayoutProfile; - var staged = UsesGenericStagedDefrag(defragmentable); - var token = this.BeginMaintenanceOperation("Defragment / re-layout", staged); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Defragmenting {Path.GetFileName(path)} ==="); - this.Append($"Mode: {mode}"); - if (staged) this.Append("Strategy: verified staged rebuild; original is unchanged until commit."); - if (interleaveStride > 1) this.Append($"Block interleave: {interleaveStride}"); - if (metadataZone != MetadataZone.Unchanged) this.Append($"Metadata zone: {metadataZone}"); - if (layoutProfile is not null) this.Append($"Layout profile: {layoutProfile.Name} ({layoutProfile.Zones.Count} zone(s))"); - if (mode == DefragMode.CarveHole) { - this.Append($"Hole size: {holeSize:N0} bytes"); - this.Append($"Hole at: {(holeAt < 0 ? "auto (end)" : holeAt.ToString("N0"))}"); - } - - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 0; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var cancelled = false; - string? finalStatus = null; - - void OnProgress(DefragProgressEvent ev) { - if (ev.Phase == "complete" && !string.IsNullOrWhiteSpace(ev.Status)) finalStatus = ev.Status; - this.BeginInvoke(() => { - if (ev.Phase == "committing") { - this._maintenanceCommitStarted = true; - this._cancel.Enabled = false; - } - - if (ev.BlockMap is not null) { - this._blockMap.BlockMap = ev.BlockMap; - this._blockMap.ImageSize = ev.ImageSize; - } else if (ev.ImageSize > 0 && this._blockMap.ImageSize <= 0) { - this._blockMap.ImageSize = ev.ImageSize; - } - - this._blockMap.ReadHead = ev.CurrentReadOffset; - this._blockMap.WriteHead = ev.CurrentWriteOffset; - if (ev.Fraction >= 0) this._progress.Value = (int)(Math.Clamp(ev.Fraction, 0, 1) * 100); - if (!string.IsNullOrWhiteSpace(ev.Status)) this._layoutStatus.Text = ev.Status; - }); - } - - try { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - defragmentable.Defragment(stream, new DefragOptions { - Mode = mode, - HoleSize = holeSize, - HoleAt = holeAt, - InterleaveStride = interleaveStride, - MetadataZonePlacement = metadataZone, - LayoutTemplate = layoutProfile, - OnProgress = OnProgress, - CancellationToken = token, - }); - } catch (OperationCanceledException) { - cancelled = true; - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - - this.Invoke(() => { - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = -1; - this._progress.Value = 100; - - if (cancelled) - this.Append(staged - ? $"CANCELLED ({sw.ElapsedMilliseconds} ms) — staged rebuild discarded; existing container unchanged." - : $"CANCELLED ({sw.ElapsedMilliseconds} ms) — completed native moves remain in place."); - else if (error is not null) - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - else { - this.Append($"OK ({sw.ElapsedMilliseconds} ms)"); - if (token.IsCancellationRequested) - this.Append("Cancellation was requested, but the native operation completed before reaching a cancellable boundary."); - if (!string.IsNullOrWhiteSpace(finalStatus)) this.Append($"Status: {finalStatus}"); - if (mode == DefragMode.ConsolidateAtEnd) - this.Append("Tip: image byte size doesn't change for in-place end-pack — see the block chart for the new layout."); - this.NotifyMutated(path); - } - - this.Append(""); - this.EndMaintenanceOperation(); - }); - }); - } - - /// - /// True when the descriptor inherits the generic staged rebuild rather than moving blocks itself, - /// which is what makes a cancellation safe to discard. - /// - private static bool UsesGenericStagedDefrag(IArchiveDefragmentable defragmentable) { - var type = defragmentable.GetType(); - return type.GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream)]) is null - && type.GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream), typeof(DefragOptions)]) is null; - } - - private void RunArchiveOptimizeWithBlockProgress(IArchiveFormatOperations? ops) { - if (this._imagePath is not { } path || ops is null) return; - - if (this._isSevenZipFormat && this._smartSolidRepack.Checked) { - this.RunSmartSevenZipWithBlockProgress(ops); - return; - } - - // CVF formats have a dedicated per-cluster optimizer whose writer already chooses compress or - // store per cluster; trying every method at the container level would only waste CPU. - if (this._formatId is "DoubleSpace" or "DriveSpace" or "DriveSpace3") { - this.OnRunCvfOptimize(path, this._formatId); - return; - } - - var originalSize = new FileInfo(path).Length; - var tempOut = path + ".opt.tmp"; - AtomicFileWriter.TryDelete(tempOut); - var token = this.BeginMaintenanceOperation("Archive optimize / repack", staged: true); - this.SetStagedArchiveMap(path, ops, originalSize); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Optimizing {Path.GetFileName(path)} ==="); - this.Append("Staged rebuild: green head = projected source consumption; orange head = staged-target bytes written."); - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 0; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var cancelled = false; - var newSize = originalSize; - var entriesOptimized = 0; - - try { - var worker = Task.Run(() => ArchiveOperations.Optimize(path, tempOut, password: null)); - while (!worker.Wait(100)) { - var stagedBytes = FindStagedOutputLength(tempOut); - var fraction = originalSize > 0 ? Math.Clamp((double)stagedBytes / originalSize, 0, 0.95) : 0; - this.BeginInvoke(() => { - this._progress.Value = (int)(fraction * 100); - var displaySize = Math.Max(1L, this._blockMap.ImageSize > 0 ? this._blockMap.ImageSize : originalSize); - this._blockMap.ReadHead = Math.Clamp((long)(fraction * displaySize), 0, displaySize - 1); - this._blockMap.WriteHead = stagedBytes > 0 ? Math.Clamp(stagedBytes, 0, displaySize - 1) : -1; - this._layoutStatus.Text = token.IsCancellationRequested - ? "Cancellation pending — current codec unit will finish, then the staged target is discarded." - : $"Rebuilding staged target — {FormatSize(stagedBytes)} written; original unchanged."; - }); - } - - var result = worker.GetAwaiter().GetResult(); - newSize = result.OptimizedSize; - entriesOptimized = result.EntriesOptimized; - - if (token.IsCancellationRequested) { - cancelled = true; - } else { - this.Invoke(() => { - this._maintenanceCommitStarted = true; - this._cancel.Enabled = false; - this._progress.Value = 99; - this._layoutStatus.Text = "Staged target complete — committing; cancellation is no longer safe."; - }); - AtomicFileWriter.ReplaceTarget(tempOut, path); - } - } catch (OperationCanceledException) { - cancelled = true; - } catch (Exception ex) { - error = ex; - } finally { - AtomicFileWriter.TryDelete(tempOut); - } - sw.Stop(); - - this.Invoke(() => { - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = -1; - this._progress.Value = 100; - - if (cancelled) - this.Append($"CANCELLED ({sw.ElapsedMilliseconds} ms) — staged target discarded; existing archive unchanged."); - else if (error is not null) - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - else { - var delta = newSize - originalSize; - var pct = originalSize > 0 ? (double)delta / originalSize * 100 : 0; - this.Append($"OK ({sw.ElapsedMilliseconds} ms) — {entriesOptimized} entries re-encoded"); - this.Append($"Archive size: {originalSize:N0} -> {newSize:N0} bytes (Δ {delta:+#,#;-#,#;0}, {pct:+0.0;-0.0;0.0}%)"); - this.NotifyMutated(path); - } - - this.Append(""); - this.EndMaintenanceOperation(); - }); - }); - } - - private void RunSmartSevenZipWithBlockProgress(IArchiveFormatOperations ops) { - if (this._imagePath is not { } path) return; - - var originalSize = new FileInfo(path).Length; - var token = this.BeginMaintenanceOperation("7z solid-block re-group", staged: true); - this.SetStagedArchiveMap(path, ops, originalSize); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Smart solid-block repack: {Path.GetFileName(path)} ==="); - this.Append("All candidate layouts are staged. Cancel discards them and leaves the current 7z untouched."); - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 0; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var cancelled = false; - FileFormat.SevenZip.SolidBlockOptimizer.OptimizeResult? result = null; - - try { - using var fs = File.OpenRead(path); - result = FileFormat.SevenZip.SolidBlockOptimizer.Optimize( - fs, - maxTrials: 5, - onProgress: (index, total, name) => this.BeginInvoke(() => this.Append($" Trying strategy {index + 1}/{total}: {name}...")), - onDetailedProgress: detail => this.BeginInvoke(() => { - var displaySize = Math.Max(1L, this._blockMap.ImageSize > 0 ? this._blockMap.ImageSize : originalSize); - double fraction; - - switch (detail.Phase) { - case "extracting": - fraction = 0.30 * detail.BytesDone / Math.Max(1.0, detail.BytesTotal); - this._blockMap.ReadHead = Math.Clamp((long)(detail.BytesDone / Math.Max(1.0, detail.BytesTotal) * displaySize), 0, displaySize - 1); - this._blockMap.WriteHead = -1; - break; - case "strategy": - fraction = 0.30 + 0.10 * detail.Current / Math.Max(1.0, detail.Total); - this._blockMap.ReadHead = -1; - break; - case "building": - fraction = 0.40 + 0.55 * detail.Current / Math.Max(1.0, detail.Total); - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = Math.Clamp((long)(detail.Current / Math.Max(1.0, detail.Total) * displaySize), 0, displaySize - 1); - break; - default: - fraction = 0; - break; - } - - this._progress.Value = (int)(Math.Clamp(fraction, 0, 0.95) * 100); - this._layoutStatus.Text = detail.Phase switch { - "extracting" => $"Reading source entry: {detail.Name}", - "strategy" => $"Planning solid grouping: {detail.Name}", - "building" => $"Building staged solid candidate: {detail.Name}", - _ => "Staged 7z regrouping", - }; - }), - cancellationToken: token); - } catch (OperationCanceledException) { - cancelled = true; - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - - this.Invoke(() => { - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = -1; - this._progress.Value = 100; - - if (cancelled) { - this.Append($"CANCELLED ({sw.ElapsedMilliseconds} ms) — candidate regroup discarded; existing 7z unchanged."); - } else if (error is not null) { - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - } else if (result is not null) { - foreach (var trial in result.Trials) - this.Append($" {trial.StrategyName}: {FormatSize(trial.OutputSize)} ({trial.Elapsed.TotalMilliseconds:F0} ms)"); - - var newSize = (long)result.Data.Length; - var delta = newSize - originalSize; - var pct = originalSize > 0 ? (double)delta / originalSize * 100 : 0; - this.Append($" Winner: {result.WinningStrategy}"); - this.Append($"Archive size: {originalSize:N0} -> {newSize:N0} bytes ({delta:+#,#;-#,#;0}, {pct:+0.0;-0.0;0.0}%)"); - - if (newSize < originalSize) { - this._maintenanceCommitStarted = true; - this._cancel.Enabled = false; - this._layoutStatus.Text = "Winning staged layout selected — committing; cancellation disabled."; - try { - AtomicFileWriter.WriteAllBytesAtomic(path, result.Data); - this.Append("Optimized archive written."); - this.NotifyMutated(path); - } catch (Exception writeError) { - this.Append($"FAILED while committing winner: {writeError.Message}"); - } - } else { - this.Append("No strategy improved on the original size; archive unchanged."); - } - } - - this.Append(""); - this.EndMaintenanceOperation(); - }); - }); - } - - private void OnRunCvfOptimize(string path, string formatId) { - var ops = this._archiveOps; - var descriptor = FormatRegistry.GetById(formatId); - if (descriptor is null) { - this.Append($"FAILED: format descriptor '{formatId}' not registered."); - this.Append(""); - this._run.Enabled = true; - return; - } - - Task.Run(() => { - Exception? error = null; - CvfOptimizer.OptimizeResult? result = null; - try { - result = CvfOptimizer.Optimize(path, descriptor); - } catch (Exception ex) { - error = ex; - } - - this.Invoke(() => { - this._progress.Value = 100; - this._run.Enabled = true; - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = -1; - - if (error is not null || result is null) { - this.Append($"FAILED: {error?.GetType().Name}: {error?.Message}"); - } else { - var delta = result.OptimizedSize - result.OriginalSize; - var pct = result.OriginalSize > 0 ? (double)delta / result.OriginalSize * 100 : 0; - this.Append($"Optimized via {result.MethodUsed}: {result.OriginalSize / (1024.0 * 1024.0):F2} MB → {result.OptimizedSize / (1024.0 * 1024.0):F2} MB (Δ {pct:+0.0;-0.0;0.0}%)"); - if (result.FilesCompressed >= 0 || result.FilesStoredVerbatim >= 0) - this.Append($"Cluster mix: {result.FilesCompressed} compressed, {result.FilesStoredVerbatim} stored verbatim (per-cluster shrink-or-store fallback)"); - this.Append($"Elapsed: {result.Elapsed.TotalMilliseconds:F0} ms"); - this.NotifyMutated(path); - } - - this.Append(""); - this.PreviewBlockMap(path, ops, wasMutated: error is null); - }); - }); - } - - private void OnRunFileInternalOptimize() { - if (this._imagePath is not { } path || this._chunkMover is not { } chunkMover) return; - - var ops = this._archiveOps; - var placementProfile = this.SelectedMetadataPlacement(); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Optimizing {Path.GetFileName(path)} (file-internal) ==="); - if (placementProfile is not null) this.Append($"Metadata placement: {placementProfile.Name}"); - - this._run.Enabled = false; - this._progress.Style = ProgressBarStyle.Marquee; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var originalSize = new FileInfo(path).Length; - - try { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - chunkMover.Optimize(stream, placementProfile); - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - var newSize = File.Exists(path) ? new FileInfo(path).Length : 0L; - - this.Invoke(() => { - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 100; - this._run.Enabled = true; - this._blockMap.ReadHead = -1; - this._blockMap.WriteHead = -1; - - if (error is not null) - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - else { - this.Append($"OK ({sw.ElapsedMilliseconds} ms)"); - this.Append($"File size: {originalSize:N0} -> {newSize:N0} bytes (Δ {newSize - originalSize:+#,#;-#,#;0})"); - } - - this.Append(""); - this.PreviewBlockMap(path, ops, wasMutated: error is null); - }); - }); - } - - /// - /// Projects the archive's entries onto the image's byte span, weighted by size, so the staged - /// rebuild has something to animate against before any output exists. - /// - private void SetStagedArchiveMap(string path, IArchiveFormatOperations ops, long displaySize) { - try { - using var stream = File.OpenRead(path); - var entries = ops.List(stream, null).Where(e => !e.IsDirectory).ToArray(); - if (entries.Length == 0) return; - - var weights = entries.Select(e => Math.Max(1L, e.OriginalSize)).ToArray(); - var totalWeight = Math.Max(1L, weights.Sum()); - var size = Math.Max(1L, displaySize); - var map = new List(entries.Length); - long cumulative = 0; - long offset = 0; - - for (var i = 0; i < entries.Length; ++i) { - cumulative += weights[i]; - var end = i == entries.Length - 1 ? size : (long)((double)cumulative / totalWeight * size); - end = Math.Clamp(end, offset, size); - if (end > offset) - map.Add(new(offset, end - offset, DefragBlockKind.Used, entries[i].Name, ClassifyByMethod(entries[i].Method ?? ""))); - offset = end; - } - - this._blockMap.BlockMap = map; - this._blockMap.ImageSize = size; - this._blockMap.ReadHead = 0; - this._blockMap.WriteHead = -1; - this._layoutStatus.Text = "Staged rebuild projection — colored target blocks; green source head / orange target head use separate byte-spaces."; - } catch { - // The existing preview stays usable when a synthetic target map cannot be built. - } - } - - /// - /// The optimizer writes through its own temporary file, so the staged size is the largest of the - /// target and any sibling scratch file it is currently filling. - /// - private static long FindStagedOutputLength(string target) { - try { - var best = File.Exists(target) ? new FileInfo(target).Length : 0L; - var directory = Path.GetDirectoryName(target); - if (string.IsNullOrEmpty(directory)) directory = Directory.GetCurrentDirectory(); - - foreach (var candidate in Directory.EnumerateFiles(directory, Path.GetFileName(target) + ".tmp.*")) - best = Math.Max(best, new FileInfo(candidate).Length); - - return best; - } catch { - return 0; - } - } - - // ── Individual verbs ──────────────────────────────────────────────────────────────────────── - - private void OnShrink() { - if (this._imagePath is not { } path) return; - - var formatId = this._formatLabel.Text; - var ops = FormatRegistry.GetArchiveOps(formatId); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Shrinking {Path.GetFileName(path)} ==="); - this._shrink.Enabled = false; - this._run.Enabled = false; - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 0; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var originalSize = new FileInfo(path).Length; - long newSize = 0; - var summary = ""; - - try { - this.BeginInvoke(() => this._progress.Value = 20); - - if (formatId == "Fat") { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileSystem.Fat.FatShrinkHelper.Shrink(stream); - newSize = result.NewSize; - summary = result.WasReduced - ? $"Reduced: {FormatSize(result.OriginalSize)} -> {FormatSize(result.NewSize)}" - : "No reduction (image was already compact)"; - } else if (formatId is "Ext" or "Ext1") { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileSystem.Ext.ExtShrinkHelper.Shrink(stream); - newSize = result.NewSize; - summary = result.WasReduced - ? $"Reduced: {FormatSize(result.OriginalSize)} -> {FormatSize(result.NewSize)}" - : "No reduction (image was already compact)"; - } else if (formatId == "Vhd") { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileFormat.Vhd.VhdCompactor.Compact(stream); - newSize = result.NewSize; - summary = result.WasReduced - ? $"Compacted: {FormatSize(result.OriginalSize)} -> {FormatSize(result.NewSize)} ({result.BlocksFreed} blocks freed)" - : "No reduction (already compact)"; - } else if (ops is IArchiveShrinkable shrinkable) { - // Shrink to a temp file then swap it in, so a crash mid-write cannot corrupt the source. - var tempOut = path + ".shrink.tmp"; - try { - using (var input = File.OpenRead(path)) - using (var output = File.Create(tempOut)) - shrinkable.Shrink(input, output); - - newSize = new FileInfo(tempOut).Length; - AtomicFileWriter.WriteAllBytesAtomic(path, File.ReadAllBytes(tempOut)); - summary = newSize < originalSize - ? $"Reduced: {FormatSize(originalSize)} -> {FormatSize(newSize)}" - : "No reduction (already compact)"; - } finally { - AtomicFileWriter.TryDelete(tempOut); - } - } else { - throw new NotSupportedException($"Shrink not supported for format: {formatId}"); - } - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - if (newSize == 0 && File.Exists(path)) newSize = new FileInfo(path).Length; - - this.Invoke(() => { - this._progress.Value = 100; - this._shrink.Enabled = true; - this._run.Enabled = true; - - if (error is not null) - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - else { - this.Append($"OK ({sw.ElapsedMilliseconds} ms)"); - this.Append($" {summary}"); - this._sizeLabel.Text = $"{FormatSize(newSize)} ({newSize:N0} bytes)"; - } - - this.Append(""); - this.PreviewBlockMap(path, ops, wasMutated: error is null); - }); - }); - } - - private void OnWipeEmpty() { - if (this._imagePath is not { } path) return; - - var formatId = this._formatLabel.Text; - var ops = FormatRegistry.GetArchiveOps(formatId); - - this.Append($"=== {DateTime.Now:HH:mm:ss} Wiping unused space in {Path.GetFileName(path)} ==="); - this._wipeEmpty.Enabled = false; - this._shrink.Enabled = false; - this._run.Enabled = false; - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 0; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var originalSize = new FileInfo(path).Length; - long wiped = 0; - - // Total span of unused bytes. Stays negative when the format cannot expose its extent map - // separately from the wipe itself. - var totalUnused = -1L; - - try { - this.BeginInvoke(() => this._progress.Value = 20); - - if (ops is IWipeEmpty wiper) { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - - // The wiper reports only the bytes it had to overwrite. On a mostly-empty image that - // figure and the actual free space diverge sharply, and the smaller one reads as "this - // image is barely empty", which is wrong — so compute the total up front when we can. - if (ops is IFilesystemExtentMap fsMap) { - stream.Position = 0; - totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(fsMap.EnumerateExtents(stream), stream.Length); - } else if (ops is IArchiveLayoutMap arMap) { - stream.Position = 0; - totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(arMap.EnumerateLayout(stream), stream.Length); - } - - stream.Position = 0; - wiped = wiper.WipeUnusedSpace(stream); - } else if (ops is IFilesystemExtentMap extentMap) { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - stream.Position = 0; - var extents = extentMap.EnumerateExtents(stream).ToList(); - totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(extents, stream.Length); - wiped = UnusedSpaceWiper.Wipe(stream, extents, stream.Length); - } else if (ops is IArchiveLayoutMap layoutMap) { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - stream.Position = 0; - var extents = layoutMap.EnumerateLayout(stream).ToList(); - totalUnused = UnusedSpaceWiper.ComputeUnusedBytes(extents, stream.Length); - wiped = UnusedSpaceWiper.Wipe(stream, extents, stream.Length, wipeClusterTips: false); - } else { - throw new NotSupportedException($"Format {formatId} does not support wipe-empty."); - } - - this.BeginInvoke(() => this._progress.Value = 80); - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - - this.Invoke(() => { - this._progress.Value = 100; - this._wipeEmpty.Enabled = true; - this._run.Enabled = true; - - if (error is not null) { - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - } else { - this.Append($"OK ({sw.ElapsedMilliseconds} ms)"); - if (totalUnused >= 0) { - var unusedPct = originalSize > 0 ? 100.0 * totalUnused / originalSize : 0; - var alreadyZero = Math.Max(0, totalUnused - wiped); - this.Append($" Unused space: {FormatSize(totalUnused)} ({unusedPct:F1}% of image)"); - this.Append($" Newly zeroed: {FormatSize(wiped)} ({wiped:N0} bytes); {FormatSize(alreadyZero)} was already zero"); - } else { - var writtenPct = originalSize > 0 ? 100.0 * wiped / originalSize : 0; - this.Append($" Wiped {FormatSize(wiped)} ({wiped:N0} bytes, {writtenPct:F1}% of image)"); - } - } - - this.Append(""); - this.PreviewBlockMap(path, ops, wasMutated: error is null); - }); - }); - } - - private void OnPurge() { - if (this._imagePath is not { } path) return; - - var formatId = this._formatLabel.Text; - var ops = FormatRegistry.GetArchiveOps(formatId); - if (ops is not IArchiveModifiable) { - this.Append($"Purge not supported: {formatId} is not modifiable."); - this.Append(""); - return; - } - - // Enumerate what is about to be erased so the confirmation is honest. - List entries; - try { - using var probe = File.OpenRead(path); - entries = ops.List(probe, password: null); - } catch (Exception ex) { - this.Append($"Purge aborted — could not list entries: {ex.Message}"); - this.Append(""); - return; - } - - var liveNames = entries.Where(e => !e.IsDirectory).Select(e => e.Name).ToArray(); - var allNames = entries.Select(e => e.Name).ToArray(); - if (allNames.Length == 0) { - this.Append("Container is already empty — nothing to purge."); - this.Append(""); - return; - } - - var confirm = MessageBox.Show(this, - $"Erase ALL {liveNames.Length} file(s) from {Path.GetFileName(path)}?\n\n" - + "This leaves a valid but empty container. The data cannot be recovered.", - "Confirm Purge", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (confirm != DialogResult.Yes) return; - - this.Append($"=== {DateTime.Now:HH:mm:ss} Purging {Path.GetFileName(path)} ==="); - foreach (var button in new[] { this._purge, this._wipeEmpty, this._shrink, this._run }) button.Enabled = false; - this._progress.Style = ProgressBarStyle.Marquee; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - var originalSize = new FileInfo(path).Length; - - try { - ArchiveOperations.Remove(path, allNames); - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - var newSize = File.Exists(path) ? new FileInfo(path).Length : 0L; - - this.Invoke(() => { - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 100; - this._purge.Enabled = true; - this._run.Enabled = true; - - if (error is not null) - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}"); - else { - this.Append($"OK ({sw.ElapsedMilliseconds} ms) — {liveNames.Length} file(s) erased"); - this.Append($"Container size: {originalSize:N0} -> {newSize:N0} bytes (Δ {newSize - originalSize:+#,#;-#,#;0})"); - } - - this.Append(""); - this.PreviewBlockMap(path, FormatRegistry.GetArchiveOps(formatId), wasMutated: error is null); - }); - }); - } - - /// - /// Scatters every allocation block across the volume, so the map shows the interleaving this - /// window exists to fix. - /// - /// - /// It asks first, and it names the file. Every other button here either improves the image or - /// leaves it as it was; this one deliberately makes it worse, and only a test wants that done to - /// a real image. - /// - private void OnScramble() { - if (this._imagePath is not { } path) return; - - var formatId = this._formatLabel.Text; - if (FormatRegistry.GetArchiveOps(formatId) is not IFilesystemScrambleable scrambler) { - this.Append($"Scramble not supported: {formatId} cannot scatter a volume in place."); - this.Append(""); - return; - } - - if (!int.TryParse(this._scrambleSeed.Text, out var seed)) { - this.Append($"Scramble aborted — '{this._scrambleSeed.Text}' is not a seed."); - this.Append(""); - return; - } - - var confirm = MessageBox.Show(this, - $"Fragment {Path.GetFileName(path)} on purpose?\n\n" - + "Every file's blocks are scattered across the volume. The contents are preserved " - + "exactly -- only the layout changes, and Defragment undoes it -- but this is a " - + "testing tool, not a repair.", - "Confirm Scramble", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (confirm != DialogResult.Yes) return; - - this.Append($"=== {DateTime.Now:HH:mm:ss} Scrambling {Path.GetFileName(path)} (seed {seed}) ==="); - - // Everything else is locked out while the layout is rewritten, then put back exactly as it was: - // which buttons apply to this image is a property of the format, not of what was just run. - var others = new[] { this._compact, this._purge, this._wipeEmpty, this._shrink, this._run }; - var wasEnabled = others.Select(b => b.Enabled).ToArray(); - foreach (var button in others) button.Enabled = false; - this._scramble.Enabled = false; - this._scrambleSeed.Enabled = false; - this._progress.Style = ProgressBarStyle.Marquee; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - - // The same bridge the defragment run uses, so the map animates the scattering as it happens - // rather than jumping to the finished layout. - void OnProgress(DefragProgressEvent ev) => this.BeginInvoke(() => { - if (ev.BlockMap is not null) { - this._blockMap.BlockMap = ev.BlockMap; - this._blockMap.ImageSize = ev.ImageSize; - } - - this._blockMap.ReadHead = ev.CurrentReadOffset; - this._blockMap.WriteHead = ev.CurrentWriteOffset; - if (ev.Fraction >= 0) this._progress.Value = (int)(Math.Clamp(ev.Fraction, 0, 1) * 100); - }); - - try { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - scrambler.Scramble(stream, new ScrambleOptions { Seed = seed, OnProgress = OnProgress }); - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - - this.Invoke(() => { - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 100; - for (var i = 0; i < others.Length; ++i) others[i].Enabled = wasEnabled[i]; - this._scramble.Enabled = true; - this._scrambleSeed.Enabled = true; - - this.Append(error is not null - ? $"FAILED ({sw.ElapsedMilliseconds} ms): {error.GetType().Name}: {error.Message}" - : $"OK ({sw.ElapsedMilliseconds} ms) — the volume is fragmented; Defragment puts it back"); - this.Append(""); - this.PreviewBlockMap(path, FormatRegistry.GetArchiveOps(formatId), wasMutated: error is null); - }); - }); - } - - private void OnCompact() { - if (this._imagePath is not { } path) return; - - var minimal = this._minimalGeometry.Checked; - var formatId = this._formatLabel.Text; - - if (minimal) { - var confirm = MessageBox.Show(this, - "Minimal geometry rebuilds the container at the smallest size the format allows " - + "(e.g. a 1.44 MB FAT floppy collapses to a few KB).\n\n" - + "Contents are preserved, but the result may no longer be a standard, mountable image. Continue?", - "Compact — minimal geometry", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); - if (confirm != DialogResult.Yes) return; - } - - this.Append($"=== {DateTime.Now:HH:mm:ss} Compacting {Path.GetFileName(path)}{(minimal ? " (minimal geometry)" : "")} ==="); - foreach (var button in new[] { this._compact, this._run, this._shrink, this._wipeEmpty, this._purge }) button.Enabled = false; - this._progress.Style = ProgressBarStyle.Marquee; - - Task.Run(() => { - var sw = Stopwatch.StartNew(); - Exception? error = null; - CompactOperation.CompactResult? result = null; - - try { - result = CompactOperation.Compact(path, new CompactOperation.CompactOptions { - Minimal = minimal, - Log = line => this.BeginInvoke(() => this.Append(" " + line)), - }); - } catch (Exception ex) { - error = ex; - } - sw.Stop(); - - this.Invoke(() => { - this._progress.Style = ProgressBarStyle.Blocks; - this._progress.Value = 100; - this._compact.Enabled = true; - this._run.Enabled = true; - - if (error is not null || result is null) { - this.Append($"FAILED ({sw.ElapsedMilliseconds} ms): {error?.GetType().Name}: {error?.Message}"); - } else { - var delta = result.NewSize - result.OriginalSize; - var pct = result.OriginalSize > 0 ? 100.0 * delta / result.OriginalSize : 0; - this.Append($"OK ({sw.ElapsedMilliseconds} ms) — steps: {(result.StepsRun.Count > 0 ? string.Join(", ", result.StepsRun) : "none")}"); - this.Append($"Container size: {FormatSize(result.OriginalSize)} -> {FormatSize(result.NewSize)} ({pct:+0.0;-0.0;0.0}%)"); - } - - this.Append(""); - this.PreviewBlockMap(path, FormatRegistry.GetArchiveOps(formatId), wasMutated: error is null); - }); - }); - } - - // ── Option readers ────────────────────────────────────────────────────────────────────────── - - private DefragMode SelectedMode() { - if (this._modePackEnd.Checked) return DefragMode.ConsolidateAtEnd; - if (this._modeFillHoles.Checked) return DefragMode.FillHolesLazy; - if (this._modeCarveHole.Checked) return DefragMode.CarveHole; - if (this._modeAscending.Checked) return DefragMode.AscendingOrder; - return DefragMode.ConsolidateAtStart; - } - - private MetadataPlacementProfile? SelectedMetadataPlacement() => this._metadataPlacement.SelectedIndex switch { - 1 => MetadataPlacementProfile.MetadataFirst, - 2 => MetadataPlacementProfile.DataFirst, - // Index 0 is the format default, which the optimizer expresses as null. - _ => null, - }; - - private MetadataZone SelectedMetadataZone() => this._metadataZone.SelectedIndex switch { - 1 => MetadataZone.Front, - 2 => MetadataZone.Back, - 3 => MetadataZone.Middle, - 4 => MetadataZone.BeforeContent, - _ => MetadataZone.Unchanged, - }; - - private static long ParseSize(string s) { - if (string.IsNullOrWhiteSpace(s)) return 0; - s = s.Trim().ToLowerInvariant(); - - long mult = 1; - if (s.EndsWith('k')) { mult = 1024L; s = s[..^1]; } - else if (s.EndsWith('m')) { mult = 1024L * 1024; s = s[..^1]; } - else if (s.EndsWith('g')) { mult = 1024L * 1024 * 1024; s = s[..^1]; } - - return long.TryParse(s, out var n) ? n * mult : 0; - } - - private static long ParseHoleAt(string s) - => string.IsNullOrWhiteSpace(s) || s.Trim().Equals("auto", StringComparison.OrdinalIgnoreCase) - ? -1 - : long.TryParse(s, out var n) ? n : -1; - - private static int ParseInterleaveStride(string s) - => string.IsNullOrWhiteSpace(s) ? 1 - : int.TryParse(s.Trim(), out var n) ? Math.Clamp(n, 1, 256) : 1; - - private void Append(string line) { - this._outputBox.AppendText(line + Environment.NewLine); - this._outputBox.SelectionStart = this._outputBox.Text.Length; - } - - private static string FormatSize(long bytes) => bytes switch { - < 1024 => $"{bytes} B", - < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", - < 1024 * 1024 * 1024 => $"{bytes / (1024.0 * 1024):F1} MB", - _ => $"{bytes / (1024.0 * 1024 * 1024):F2} GB", - }; - - // ── Mode probing ──────────────────────────────────────────────────────────────────────────── - - /// - /// Asks each mode whether the descriptor accepts it, without mutating anything. There is no way - /// to ask directly, so the descriptor is handed a stream that throws on first touch: reaching the - /// I/O means the mode was accepted, while throwing before any - /// I/O means it was not. - /// - private static List ProbeSupportedModes(IArchiveDefragmentable defragmentable) { - var supported = new List(); - - foreach (var mode in Enum.GetValues()) - try { - using var probe = new ProbeStream(); - defragmentable.Defragment(probe, new DefragOptions { Mode = mode }); - supported.Add(mode.ToString()); // Unlikely: the probe should have thrown first. - } catch (NotSupportedException) { - // Mode unsupported by the descriptor. - } catch (ProbeAbortException) { - supported.Add(mode.ToString()); - } catch { - // Anything else: assume unsupported rather than guess. - } - - return supported; - } - - private sealed class ProbeAbortException : Exception; - - private sealed class ProbeStream : Stream { - public override bool CanRead => true; - public override bool CanSeek => true; - public override bool CanWrite => true; - public override long Length => 1024; - public override long Position { get => 0; set => throw new ProbeAbortException(); } - public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); - public override long Seek(long offset, SeekOrigin origin) => throw new ProbeAbortException(); - public override void SetLength(long value) => throw new ProbeAbortException(); - public override void Write(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); - } - - /// The block-map colour key. - private sealed class LegendStrip : OwnerDrawnControl { - private static readonly (Color Color, string Label)[] Swatches = [ - (Color.FromArgb(0xE6, 0x4A, 0x19), "Hot"), - (Color.FromArgb(0x42, 0xA5, 0xF5), "Normal"), - (Color.FromArgb(0x66, 0xBB, 0x6A), "Cold"), - (Color.FromArgb(0x90, 0xA4, 0xAE), "Frozen"), - (Color.FromArgb(0xDA, 0xA5, 0x20), "Directory"), - (Color.FromArgb(0xE0, 0xE0, 0xE0), "Free"), - (Color.FromArgb(0xC0, 0x30, 0x30), "Bad"), - (Color.FromArgb(0x70, 0x70, 0x70), "Reserved"), - ]; - - protected override void OnPaint(PaintEventArgs e) { - var g = e.Graphics; - var theme = DefaultTheme.Instance; - g.FillRectangle(theme.ControlBackground, new(0, 0, this.Width, this.Height)); - - var x = 0; - var y = 2; - foreach (var (color, label) in Swatches) { - var width = 10 + 4 + Math.Max(30, g.MeasureText(label, theme.DefaultFont).Width) + 12; - if (x + width > this.Width) { - x = 0; - y += 18; - if (y + 14 > this.Height) return; - } - - g.FillRectangle(color, new(x, y + 3, 10, 10)); - g.DrawText(label, theme.DefaultFont, theme.ControlText, new(x + 14, y, width - 18, 16), ContentAlignment.MiddleLeft); - x += width; - } - - // The two heads are lines rather than swatches, matching how they are drawn on the map. - if (x + 90 > this.Width) { - x = 0; - y += 18; - } - if (y + 14 > this.Height) return; - - g.DrawLine(Color.LimeGreen, x + 2, y + 2, x + 2, y + 14, 2); - g.DrawText("Read head", theme.DefaultFont, theme.ControlText, new(x + 8, y, 70, 16), ContentAlignment.MiddleLeft); - g.DrawLine(Color.Orange, x + 84, y + 2, x + 84, y + 14, 2); - g.DrawText("Write head", theme.DefaultFont, theme.ControlText, new(x + 90, y, 74, 16), ContentAlignment.MiddleLeft); - } - } -} diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs new file mode 100644 index 000000000..c11658e1e --- /dev/null +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -0,0 +1,603 @@ +using Compression.Core.Layout; +using Compression.Lib.Layout; +using Compression.NativeUI.Controls; +using Compression.NativeUI.Maintenance; +using Compression.NativeUI.Theming; +using Compression.NativeUI.ViewModels; +using Compression.Registry; +using Compression.Registry.Layout; +using Hawkynt.NativeForms; +using Hawkynt.NativeForms.Drawing; +using Color = System.Drawing.Color; + +namespace Compression.NativeUI.Views; + +/// +/// The Defragment tab: a contextual "Disk Tools" tab that appears while there is something to +/// maintain — the open archive or image, or an archive file selected in a folder — and, while it is +/// selected, gives the whole client area to the block map. Every operation, defragmentation strategy, +/// layout option and view mode is a ribbon item; each is enabled exactly when the target supports it. +/// +/// +/// The tab holds one while it is open, so a nested archive is +/// extracted once and written back after each change, and the work itself goes through +/// . Leaving the tab hides the block view and shows the browser +/// again exactly as it was; nothing about it is rebuilt. +/// +internal sealed partial class MainForm { + private const int FieldWidth = 160; + private const int CaptionWidth = 72; + + private readonly RibbonContextualTabGroup _diskTools = new("Disk Tools", Color.FromArgb(0x3A, 0x8E, 0x5C)) { Visible = false }; + private readonly DefragmentView _defragView = new() { Visible = false }; + private readonly ToolTip _ribbonTips = new(); + private RibbonTab _defragTab = null!; + + private readonly Dictionary _verbToggles = []; + private readonly Dictionary _strategyToggles = []; + private readonly Dictionary _viewToggles = []; + private RibbonToggleButton _packAtEndToggle = null!; + private RibbonToggleButton _minimalGeometryToggle = null!; + private RibbonToggleButton _filesPanelToggle = null!; + private RibbonToggleButton _legendToggle = null!; + private RibbonButton _startButton = null!; + private RibbonButton _stopButton = null!; + private RibbonButton _analyzeButton = null!; + private RibbonButton _editProfilesButton = null!; + + private readonly TextBox _interleaveBox = new() { Text = "1" }; + private readonly ComboBox _metadataZoneCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly ComboBox _layoutProfileCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly TextBox _holeSizeBox = new() { Text = "64m" }; + private readonly TextBox _holeAtBox = new() { Text = "auto" }; + private readonly TextBox _seedBox = new() { Text = "1" }; + private readonly ComboBox _placementCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; + private RibbonHostItem _interleaveItem = null!; + private RibbonHostItem _metadataZoneItem = null!; + private RibbonHostItem _layoutProfileItem = null!; + private RibbonHostItem _holeSizeItem = null!; + private RibbonHostItem _holeAtItem = null!; + private RibbonHostItem _seedItem = null!; + private RibbonHostItem _placementItem = null!; + private readonly List _layoutProfileEntries = []; + + private MaintenanceSession? _maintenanceSession; + private MaintenancePresenter? _presenter; + private MaintenanceVerb? _pendingVerb; + private CancellationTokenSource? _maintenanceCancellation; + private bool _maintenanceCommitStarted; + private bool _defragActive; + private bool _syncingDefragRibbon; + private int _analysisGeneration; + + /// The last analysis started, so a test can wait for the map it draws. + internal Task? AnalysisTask { get; private set; } + + /// The last operation started, completed once its outcome has been shown. + internal Task? MaintenanceTask { get; private set; } + + // ── construction ──────────────────────────────────────────────────────────────────────────── + + private void BuildDefragmentTab() { + this._defragTab = new RibbonTab("Defragment"); + this._defragTab.Groups.AddRange(this.OperationGroup(), this.StrategyGroup(), this.OptionsGroup(), this.ViewGroup()); + + this._ribbon.Tabs.Add(this._defragTab); + this._diskTools.Add(this._defragTab); + this._ribbon.ContextualTabGroups.Add(this._diskTools); + + this._ribbon.SelectedIndexChanged += (_, _) => this.OnRibbonTabChanged(); + this._model.MaintenanceRequested += (_, verb) => this.OpenDefragmentTab(verb); + this._model.LocationChanged += (_, _) => this.UpdateDiskTools(); + CommandManager.RequerySuggested += (_, _) => this.UpdateDiskTools(); + this.FormClosing += this.OnClosingWhileMaintaining; + this.UpdateDiskTools(); + } + + private RibbonGroup OperationGroup() { + (MaintenanceVerb Verb, string Text, string Icon)[] verbs = [ + (MaintenanceVerb.Defragment, "Defragment", IconKeys.Defragment), + (MaintenanceVerb.Optimize, "Optimize", IconKeys.Analyze), + (MaintenanceVerb.Shrink, "Shrink", IconKeys.Defragment), + (MaintenanceVerb.Compact, "Compact", IconKeys.Defragment), + (MaintenanceVerb.WipeEmpty, "Clear", IconKeys.Remove), + (MaintenanceVerb.Purge, "Purge", IconKeys.Remove), + (MaintenanceVerb.Scramble, "Scramble", IconKeys.Defragment), + ]; + + var group = new RibbonGroup("Operation"); + foreach (var (verb, text, icon) in verbs) { + var toggle = this.RibbonToggle(text, icon, RibbonItemSize.Small, Keys.None, text, @checked: false, on => this.ChooseVerb(verb, on)); + this._verbToggles[verb] = toggle; + group.Items.Add(toggle); + } + + this._startButton = this.RibbonAction("Start", IconKeys.Test, this.StartMaintenance, RibbonItemSize.Large, Keys.None, "Run the chosen operation"); + this._stopButton = this.RibbonAction("Stop", IconKeys.Remove, this.StopMaintenance, RibbonItemSize.Small, Keys.None, "Stop the running operation"); + this._analyzeButton = this.RibbonAction("Analyze", IconKeys.Refresh, this.AnalyzeTarget, RibbonItemSize.Small, Keys.None, "Read the image's layout again"); + group.Items.AddRange(this._startButton, this._stopButton); + return group; + } + + private RibbonGroup StrategyGroup() { + (DefragStrategy Strategy, string Text)[] strategies = [ + (DefragStrategy.Consolidate, "Consolidate"), + (DefragStrategy.Defrag, "Defrag"), + (DefragStrategy.Reorder, "Re-order"), + (DefragStrategy.SortEntries, "Sort Entries"), + (DefragStrategy.CarveHole, "Carve Hole"), + ]; + + var group = new RibbonGroup("Defrag Mode"); + foreach (var (strategy, text) in strategies) { + var toggle = this.RibbonToggle(text, IconKeys.Defragment, RibbonItemSize.Small, Keys.None, text, @checked: false, on => this.ChooseStrategy(strategy, on)); + this._strategyToggles[strategy] = toggle; + group.Items.Add(toggle); + } + + this._packAtEndToggle = this.RibbonToggle("Pack at End", IconKeys.Defragment, RibbonItemSize.Small, Keys.None, "Consolidate at the back instead of the front", + @checked: false, on => this.WithPresenter(p => p.PackAtEnd = on)); + group.Items.Add(this._packAtEndToggle); + + this._holeSizeItem = this.Field("Hole size", this._holeSizeBox, "Size in bytes. Suffixes: k=KiB, m=MiB, g=GiB. Examples: 64m, 1g, 524288.", + () => this.WithPresenter(p => p.HoleSizeText = this._holeSizeBox.Text)); + this._holeAtItem = this.Field("Hole at", this._holeAtBox, "Byte offset where the hole starts. 'auto' = after the last live extent.", + () => this.WithPresenter(p => p.HoleAtText = this._holeAtBox.Text)); + group.Items.AddRange(this._holeSizeItem, this._holeAtItem); + return group; + } + + /// + /// The layout options of a defragmentation, then — below Edit Profiles, shown only while their + /// operation is picked — the one option Scramble, Optimize or Compact takes. Showing only the one + /// that applies keeps the tab narrow enough that no group with a hosted field has to fold away. + /// + private RibbonGroup OptionsGroup() { + this._metadataZoneCombo.Items.AddRange([ + "Don't move metadata", + "Metadata at front", + "Metadata at back", + "Metadata in middle", + "Before file content", + ]); + this._metadataZoneCombo.SelectedIndex = 0; + this.RefreshLayoutProfiles(); + + this._interleaveItem = this.Field("Interleave", this._interleaveBox, + $"Block interleave: 1 = contiguous, 2 = every other block, N = each file's Kth block at start + K×N. {MaintenanceInput.MinInterleave}–{MaintenanceInput.MaxInterleave}.", + () => this.WithPresenter(p => p.InterleaveText = this._interleaveBox.Text)); + this._metadataZoneItem = this.Field("Metadata", this._metadataZoneCombo, "Where filesystem metadata and directory entries are placed.", + () => this.WithPresenter(p => p.MetadataZone = this.SelectedMetadataZone())); + this._layoutProfileItem = this.Field("Profile", this._layoutProfileCombo, + "A zone-based layout template: files go into named byte ranges with per-zone sort orders.", + this.OnLayoutProfileChanged); + this._editProfilesButton = this.RibbonAction("Edit Profiles", IconKeys.Properties, () => { + new LayoutProfileEditor().ShowDialog(this); + this.RefreshLayoutProfiles(); + }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); + + this._placementCombo.Items.AddRange(["Format default", "Metadata first", "Data first"]); + this._placementCombo.SelectedIndex = 0; + + this._seedItem = this.Field("Seed", this._seedBox, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", + () => this.WithPresenter(p => p.SeedText = this._seedBox.Text)); + this._placementItem = this.Field("Chunks", this._placementCombo, "Where an optimized file puts its metadata chunks relative to the payload.", + () => this.WithPresenter(p => p.ChunkPlacement = this._placementCombo.SelectedIndex switch { + 1 => MetadataPlacementProfile.MetadataFirst, + 2 => MetadataPlacementProfile.DataFirst, + _ => null, + })); + this._minimalGeometryToggle = this.RibbonToggle("Minimal Geometry", IconKeys.Defragment, RibbonItemSize.Small, Keys.None, + "Compact rebuilds at the smallest geometry the format allows", @checked: false, on => this.WithPresenter(p => p.MinimalGeometry = on)); + + var group = new RibbonGroup("Options"); + group.Items.AddRange(this._interleaveItem, this._metadataZoneItem, this._layoutProfileItem, + this._editProfilesButton, this._seedItem, this._placementItem, this._minimalGeometryToggle); + return group; + } + + private RibbonGroup ViewGroup() { + (BlockMapView View, string Text)[] views = [ + (BlockMapView.LinearBlocks, "Blocks"), + (BlockMapView.CircularPlatter, "Circle"), + (BlockMapView.CylinderStack, "3D Stack"), + ]; + + var group = new RibbonGroup("View"); + foreach (var (view, text) in views) { + var toggle = this.RibbonToggle(text, IconKeys.ViewImage, RibbonItemSize.Small, Keys.None, text, @checked: view == BlockMapView.LinearBlocks, _ => this.ChooseView(view)); + this._viewToggles[view] = toggle; + group.Items.Add(toggle); + } + + this._filesPanelToggle = this.RibbonToggle("Files", IconKeys.ViewText, RibbonItemSize.Small, Keys.None, "Show the files beside the map", @checked: true, + on => { if (!this._syncingDefragRibbon) this._defragView.FilesPanelVisible = on; }); + this._legendToggle = this.RibbonToggle("Legend", IconKeys.Properties, RibbonItemSize.Small, Keys.None, "Show the colour key under the map", @checked: true, + on => { if (!this._syncingDefragRibbon) this._defragView.LegendVisible = on; }); + // Analyze reads the map again, so it sits with the rest of what the map shows. + group.Items.AddRange(this._filesPanelToggle, this._legendToggle, this._analyzeButton); + return group; + } + + /// A hosted input with its caption in front, one stacked row tall. + private RibbonHostItem Field(string caption, Control input, string tip, Action changed) { + var panel = new Panel(); + var label = new Label { Text = caption, Dock = DockStyle.Left, Width = CaptionWidth }; + input.Dock = DockStyle.Fill; + panel.Controls.AddRange(input, label); + + switch (input) { + case TextBox box: box.TextChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; + case ComboBox combo: combo.SelectedIndexChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; + } + + this._ribbonTips.SetToolTip(input, tip); + this._ribbonTips.SetToolTip(label, tip); + return new RibbonHostItem(panel) { HostWidth = FieldWidth, Text = caption, ToolTipText = tip, Tag = tip }; + } + + // ── the tab and the client area ───────────────────────────────────────────────────────────── + + /// Shows the contextual tab while there is something to maintain, and follows the target. + private void UpdateDiskTools() { + var running = this._presenter?.IsRunning == true || this._maintenanceCancellation is not null; + var visible = running || this._model.HasMaintenanceTarget || (this._defragActive && this._maintenanceSession is not null); + + if (!visible && this._ribbon.SelectedTab == this._defragTab) this._ribbon.SelectedIndex = 1; + this._diskTools.Visible = visible; + + // On the tab, a different target selected meanwhile — Back, Forward, a typed address — is taken + // up; a target that merely vanished (a refresh that dropped the selection) keeps the session. + if (this._defragActive && !running && this._model.MaintenanceTargetKey is { } key && key != this._maintenanceSession?.Key) + this.OpenSessionForTarget(); + } + + /// Selects the Defragment tab, preselecting when the target supports it. + internal void OpenDefragmentTab(MaintenanceVerb? verb) { + this.UpdateDiskTools(); + if (!this._diskTools.Visible) return; + + this._pendingVerb = verb; + if (this._ribbon.SelectedTab == this._defragTab) this.ApplyPendingVerb(); + else this._ribbon.SelectedIndex = this._ribbon.Tabs.IndexOf(this._defragTab); + } + + private void OnRibbonTabChanged() { + var onTab = this._ribbon.SelectedTab == this._defragTab; + if (onTab == this._defragActive) return; + + this._defragActive = onTab; + if (onTab) { + // A session still running an operation is kept, whatever is selected now: its file is in use. + var running = this._maintenanceCancellation is not null; + if (this._maintenanceSession is null || !running && this._model.MaintenanceTargetKey is { } key && key != this._maintenanceSession.Key) + this.OpenSessionForTarget(); + this.ApplyPendingVerb(); + } else if (this._maintenanceCancellation is null) { + this.CloseSession(); + } + + // The browser is hidden, not torn down, so it comes back exactly as it was left. + this._split.Visible = !onTab; + this._defragView.Visible = onTab; + this.LayoutChildren(); + this.UpdateDiskTools(); + } + + private void OpenSessionForTarget() { + this.CloseSession(); + this._defragView.Clear(); + + this._maintenanceSession = this._model.OpenMaintenanceSession(); + if (this._maintenanceSession is { } session) { + this._presenter = new MaintenancePresenter(session.ImagePath, session.FormatId) { + InterleaveText = this._interleaveBox.Text, + HoleSizeText = this._holeSizeBox.Text, + HoleAtText = this._holeAtBox.Text, + SeedText = this._seedBox.Text, + MetadataZone = this.SelectedMetadataZone(), + }; + this.OnLayoutProfileChanged(); + this._defragView.Log($"{session.DisplayName} — {session.FormatId}"); + this.AnalyzeTarget(); + } + + this.SyncDefragmentRibbon(); + } + + private void CloseSession() { + this._presenter = null; + this._maintenanceSession?.Dispose(); + this._maintenanceSession = null; + } + + private void ApplyPendingVerb() { + if (this._pendingVerb is { } verb && this._presenter is { } presenter && presenter.Capabilities.Verb(verb).Supported) + presenter.Verb = verb; + this._pendingVerb = null; + this.SyncDefragmentRibbon(); + } + + // ── choices ───────────────────────────────────────────────────────────────────────────────── + + private void WithPresenter(Action change) { + if (this._syncingDefragRibbon || this._presenter is not { } presenter) return; + change(presenter); + this.SyncDefragmentRibbon(); + } + + /// The operations behave as radio buttons: clicking one picks it, clicking it again keeps it. + private void ChooseVerb(MaintenanceVerb verb, bool on) => this.WithPresenter(p => { + if (on) p.Verb = verb; + }); + + private void ChooseStrategy(DefragStrategy strategy, bool on) => this.WithPresenter(p => { + if (on) p.Strategy = strategy; + }); + + private void ChooseView(BlockMapView view) { + if (this._syncingDefragRibbon) return; + this._defragView.Map.ViewMode = view; + this.SyncDefragmentRibbon(); + } + + private MetadataZone SelectedMetadataZone() => this._metadataZoneCombo.SelectedIndex switch { + 1 => MetadataZone.Front, + 2 => MetadataZone.Back, + 3 => MetadataZone.Middle, + 4 => MetadataZone.BeforeContent, + _ => MetadataZone.Unchanged, + }; + + private void RefreshLayoutProfiles() { + var index = this._layoutProfileCombo.SelectedIndex; + var previous = index > 0 && index < this._layoutProfileEntries.Count ? this._layoutProfileEntries[index]?.FilePath : null; + + this._syncingDefragRibbon = true; + try { + this._layoutProfileCombo.Items.Clear(); + this._layoutProfileEntries.Clear(); + this._layoutProfileCombo.Items.Add("(none)"); + this._layoutProfileEntries.Add(null); + try { + foreach (var entry in LayoutProfileStore.List()) { + this._layoutProfileCombo.Items.Add($"{entry.Name} [{(entry.Origin == ProfileOrigin.Builtin ? "Built-in" : "User")}]"); + this._layoutProfileEntries.Add(entry); + } + } catch { + // A broken profile folder leaves "(none)"; the editor reports what is wrong with it. + } + + var keep = this._layoutProfileEntries.FindIndex(e => e is not null && string.Equals(e.FilePath, previous, StringComparison.OrdinalIgnoreCase)); + this._layoutProfileCombo.SelectedIndex = Math.Max(0, keep); + } finally { + this._syncingDefragRibbon = false; + } + + this.OnLayoutProfileChanged(); + } + + private void OnLayoutProfileChanged() { + if (this._presenter is not { } presenter) return; + + var index = this._layoutProfileCombo.SelectedIndex; + if (index <= 0 || index >= this._layoutProfileEntries.Count || this._layoutProfileEntries[index] is not { } entry) { + presenter.LayoutProfile = null; + return; + } + + try { + presenter.LayoutProfile = LayoutProfileStore.Load(entry); + } catch (Exception ex) { + presenter.LayoutProfile = null; + MessageBox.Show(this, $"Failed to load layout profile '{entry.Name}':\n{ex.Message}", "Layout profile", MessageBoxButtons.OK, MessageBoxIcon.Error); + this._layoutProfileCombo.SelectedIndex = 0; + } + } + + // ── keeping the ribbon honest ─────────────────────────────────────────────────────────────── + + /// + /// Enables, checks and explains every item from the presenter: an item is enabled only when the + /// target supports it and the current choices make it apply, and its tooltip says why when not. + /// + private void SyncDefragmentRibbon() { + if (this._syncingDefragRibbon) return; + + this._syncingDefragRibbon = true; + try { + var p = this._presenter; + var caps = p?.Capabilities; + var running = p?.IsRunning == true || this._maintenanceCancellation is not null; + var idle = p is not null && !running; + var defrag = p?.Verb == MaintenanceVerb.Defragment; + const string NoTarget = "Open or select an image to maintain."; + + foreach (var (verb, toggle) in this._verbToggles) { + var capability = caps?.Verb(verb) ?? Capability.No(NoTarget); + toggle.Enabled = idle && capability.Supported; + toggle.Checked = p?.Verb == verb; + toggle.ToolTipText = $"{toggle.Text}: {capability.Reason}"; + } + + foreach (var (strategy, toggle) in this._strategyToggles) { + var capability = caps?.Strategy(strategy) ?? Capability.No(NoTarget); + toggle.Enabled = idle && defrag && capability.Supported; + toggle.Checked = defrag && p!.Strategy == strategy; + toggle.ToolTipText = $"{toggle.Text}: {(capability.Supported && !defrag ? "Applies to Defragment." : capability.Reason)}"; + } + + var consolidate = defrag && p!.Strategy == DefragStrategy.Consolidate; + var endChoosable = caps is not null && caps.PackAtEnd.Supported && caps.PackAtStart.Supported; + this._packAtEndToggle.Enabled = idle && consolidate && endChoosable; + this._packAtEndToggle.Checked = consolidate && p!.EngineMode == DefragMode.ConsolidateAtEnd; + this._packAtEndToggle.ToolTipText = $"Pack at End: {(caps is null ? NoTarget : !consolidate ? "Applies to Consolidate." : caps.PackAtEnd.Reason)}"; + + var carve = defrag && p!.Strategy == DefragStrategy.CarveHole; + this._holeSizeItem.Visible = carve; + this._holeAtItem.Visible = carve; + this.Mark(this._holeSizeItem, idle, carve && MaintenanceInput.ParseSize(this._holeSizeBox.Text).Error is { } sizeError ? sizeError : null); + this.Mark(this._holeAtItem, idle, carve && MaintenanceInput.ParseHoleAt(this._holeAtBox.Text).Error is { } atError ? atError : null); + + this.Option(this._interleaveItem, idle && defrag, caps?.Interleave, defrag, + caps?.Interleave.Supported == true && defrag ? MaintenanceInput.ParseInterleave(this._interleaveBox.Text).Error : null); + this.Option(this._metadataZoneItem, idle && defrag, caps?.MetadataZone, defrag, null); + this.Option(this._layoutProfileItem, idle && defrag, caps?.LayoutProfile, defrag, null); + this._editProfilesButton.Enabled = idle && defrag && caps?.LayoutProfile.Supported == true; + + var scramble = p?.Verb == MaintenanceVerb.Scramble; + this._seedItem.Visible = scramble; + this._placementItem.Visible = p?.Verb == MaintenanceVerb.Optimize && caps?.ChunkPlacement.Supported == true; + this._minimalGeometryToggle.Visible = p?.Verb == MaintenanceVerb.Compact && caps?.MinimalGeometry.Supported == true; + this.Option(this._seedItem, idle && scramble, scramble ? Capability.Yes("Seeds Scramble's shuffle.") : null, scramble, + scramble ? MaintenanceInput.ParseSeed(this._seedBox.Text).Error : null, appliesTo: "Scramble"); + var optimize = p?.Verb == MaintenanceVerb.Optimize; + this.Option(this._placementItem, idle && optimize, caps?.ChunkPlacement, optimize, null, appliesTo: "Optimize"); + var compact = p?.Verb == MaintenanceVerb.Compact; + this._minimalGeometryToggle.Enabled = idle && compact && caps?.MinimalGeometry.Supported == true; + this._minimalGeometryToggle.Checked = p?.MinimalGeometry == true && compact; + this._minimalGeometryToggle.ToolTipText = $"Minimal Geometry: {(caps is null ? NoTarget : !compact ? "Applies to Compact." : caps.MinimalGeometry.Reason)}"; + + var blocker = p?.StartBlocker ?? (p is null ? NoTarget : null); + this._startButton.Enabled = idle && blocker is null; + this._startButton.ToolTipText = blocker ?? $"{MaintenancePresenter.Label(p!.Verb!.Value)} {this._maintenanceSession?.DisplayName}"; + this._stopButton.Enabled = running && p?.CanCancel == true && !this._maintenanceCommitStarted && this._maintenanceCancellation is { IsCancellationRequested: false }; + this._analyzeButton.Enabled = idle; + + foreach (var (view, toggle) in this._viewToggles) toggle.Checked = this._defragView.Map.ViewMode == view; + this._filesPanelToggle.Checked = this._defragView.FilesPanelVisible; + this._legendToggle.Checked = this._defragView.LegendVisible; + } finally { + this._syncingDefragRibbon = false; + } + } + + /// Enables a hosted option and explains it: supported, not applicable, refused, or mistyped. + private void Option(RibbonHostItem item, bool applies, Capability? capability, bool relevant, string? error, string appliesTo = "Defragment") { + var supported = capability?.Supported == true; + item.Enabled = applies && supported; + item.Control.Enabled = item.Enabled; + var reason = capability is null ? "Open or select an image to maintain." + : !relevant ? $"Applies to {appliesTo}." + : capability.Value.Reason; + item.ToolTipText = error ?? $"{item.Text}: {reason} {item.Tag}"; + this.Flag(item, error); + } + + private void Mark(RibbonHostItem item, bool enabled, string? error) { + item.Enabled = enabled; + item.Control.Enabled = enabled; + item.ToolTipText = error ?? item.Tag as string; + this.Flag(item, error); + } + + /// A value that does not read turns its field pink, and the tooltip says what is wrong. + private void Flag(RibbonHostItem item, string? error) { + var input = item.Control.Controls[0]; + if (input is TextBox box) + box.BackColor = error is not null ? Color.FromArgb(0xFF, 0xD8, 0xD8) + : box.Enabled ? DefaultTheme.Instance.FieldBackground + : DefaultTheme.Instance.ControlBackground; + this._ribbonTips.SetToolTip(input, error ?? item.Tag as string); + } + + // ── running ───────────────────────────────────────────────────────────────────────────────── + + private void AnalyzeTarget() { + if (this._presenter is not { } presenter || presenter.IsRunning) return; + + var generation = ++this._analysisGeneration; + this._defragView.StatusText = $"Reading the layout of {this._maintenanceSession?.DisplayName}…"; + this.AnalysisTask = Task.Run(presenter.Analyze).ContinueWith(t => this.BeginInvoke(() => { + if (generation != this._analysisGeneration || this._presenter != presenter) return; + this._defragView.ShowSnapshot(t.IsCompletedSuccessfully ? t.Result : BlockMapSnapshot.Empty($"The layout could not be read: {t.Exception?.GetBaseException().Message}")); + }), TaskScheduler.Default); + } + + private void StartMaintenance() { + if (this._presenter is not { } presenter || this._maintenanceSession is not { } session) return; + if (presenter.StartBlocker is { } blocker) { + this._model.StatusText = blocker; + return; + } + + if (presenter.ConfirmationFor() is { } question + && MessageBox.Show(this, question, $"Confirm {presenter.Verb}", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) + return; + + ++this._analysisGeneration; // a pending analysis must not paint over the live run + this._maintenanceCancellation = new(); + this._maintenanceCommitStarted = false; + this._defragView.ProgressValue = 0; + + var run = presenter.RunAsync( + progress => this.BeginInvoke(() => this.OnMaintenanceProgress(progress)), + line => this.BeginInvoke(() => this._defragView.Log(line)), + this._maintenanceCancellation.Token); + this.SyncDefragmentRibbon(); + this.MaintenanceTask = run.ContinueWith(t => this.BeginInvoke(() => this.OnMaintenanceCompleted(presenter, session, t.Result)), TaskScheduler.Default); + } + + private void OnMaintenanceProgress(MaintenanceProgress progress) { + if (progress.CommitStarted) this._maintenanceCommitStarted = true; + this._defragView.Apply(progress); + this.SyncDefragmentRibbon(); + } + + private void OnMaintenanceCompleted(MaintenancePresenter presenter, MaintenanceSession session, MaintenanceOutcome outcome) { + this._maintenanceCancellation?.Dispose(); + this._maintenanceCancellation = null; + this._maintenanceCommitStarted = false; + this._defragView.ProgressValue = 100; + + // Re-listing the archive rewrites the status line, so the outcome is reported after it. + if (outcome.Mutated) session.NotifyMutated(); + + var name = session.DisplayName; + this._model.StatusText = outcome.Kind switch { + MaintenanceOutcomeKind.Succeeded => $"{presenter.Verb} finished on {name}: {outcome.Summary}", + MaintenanceOutcomeKind.Refused => $"{presenter.Verb} refused on {name}; the image was left unchanged. {outcome.Summary}", + MaintenanceOutcomeKind.Cancelled => $"{presenter.Verb} stopped on {name}. {outcome.Summary}", + _ => $"{presenter.Verb} failed on {name}: {outcome.Summary}", + }; + + if (outcome.Kind == MaintenanceOutcomeKind.Refused) + MessageBox.Show(this, $"{outcome.Summary}\n\nThe image was left unchanged.", $"{presenter.Verb} not supported", MessageBoxButtons.OK, MessageBoxIcon.Information); + else if (outcome.Kind == MaintenanceOutcomeKind.Failed) + MessageBox.Show(this, outcome.Summary, $"{presenter.Verb} failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + + if (this._defragActive && this._presenter == presenter) this.AnalyzeTarget(); + else if (!this._defragActive && this._maintenanceSession == session) this.CloseSession(); + + this.UpdateDiskTools(); + this.SyncDefragmentRibbon(); + } + + private void StopMaintenance() { + if (this._presenter is not { } presenter || this._maintenanceCancellation is not { IsCancellationRequested: false } cancellation) return; + if (this._maintenanceCommitStarted) return; + + if (!presenter.IsStaged + && MessageBox.Show(this, + $"Stop {presenter.Verb}?\n\nThis operation moves data in place. Moves already completed are not rolled back; it stops at the next safe " + + "boundary, so the layout may be partly changed although the contents stay intact.", + "Stop in-place maintenance", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) + return; + + this._defragView.Log(presenter.IsStaged + ? "Stop requested — the staged copy will be discarded; the image stays as it is." + : "Stop requested — moves already completed stay where they are."); + cancellation.Cancel(); + this.SyncDefragmentRibbon(); + } + + private void OnClosingWhileMaintaining(object? sender, FormClosingEventArgs e) { + if (e.Cancel || this._maintenanceCancellation is null) return; + + // Closing mid-run would cut an operation off between two writes. Ask it to stop instead; the + // window can close once it has. + e.Cancel = true; + if (!this._maintenanceCancellation.IsCancellationRequested && this._presenter?.CanCancel == true) this._maintenanceCancellation.Cancel(); + this._model.StatusText = "An operation is still running; it stops at the next safe point, then the window can close."; + } +} diff --git a/Compression.NativeUI/Views/MainForm.Ribbon.cs b/Compression.NativeUI/Views/MainForm.Ribbon.cs index f4d6deeb0..fc39e8784 100644 --- a/Compression.NativeUI/Views/MainForm.Ribbon.cs +++ b/Compression.NativeUI/Views/MainForm.Ribbon.cs @@ -8,8 +8,9 @@ namespace Compression.NativeUI.Views; /// /// The command surface: an Office-style ribbon in place of a menu bar and toolbar. Every command the /// shell has lives here once — the Quick Access Toolbar for navigation, File for archives as files, -/// Home for everyday file work, View for panes and layout, Tools for the specialist windows, and an -/// Archive Tools tab that appears only while the shell is inside an archive. +/// Home for everyday file work, View for panes and layout, Tools for the specialist windows, an +/// Archive Tools tab that appears only while the shell is inside an archive, and a Disk Tools tab +/// (Defragment) that appears while there is an image to maintain. /// /// /// Shortcut keys are registered on the ribbon items and dispatched form-wide. Keys that belong to the @@ -39,6 +40,7 @@ private void BuildRibbon() { this._ribbon.Tabs.Add(archive); this._archiveTools.Add(archive); this._ribbon.ContextualTabGroups.Add(this._archiveTools); + this.BuildDefragmentTab(); this._ribbon.SelectedIndex = 1; // Home, as in Explorer this._ribbon.PreferredHeightChanged += (_, _) => this.LayoutChildren(); @@ -119,7 +121,8 @@ private RibbonTab ToolsTab() { this.RibbonCommand("Analyze Entry", IconKeys.Analyze, this._model.AnalyzeCommand, RibbonItemSize.Small, Keys.None, "Analyze the selected entry"), this.RibbonAction("Reverse Engineer", IconKeys.Analyze, () => new ReverseEngineerWindow().Show(), RibbonItemSize.Small, Keys.None, "Work out an unknown format")), Group("Disks and Images", - this.RibbonAction("Maintenance", IconKeys.Defragment, this.OpenMaintenance, RibbonItemSize.Large, Keys.None, "Optimize, shrink, defragment, purge or wipe an image"), + this.RibbonCommand("Maintenance", IconKeys.Defragment, this._model.MaintenanceCommand, RibbonItemSize.Large, Keys.Control | Keys.Shift | Keys.D, + "Open the Defragment tab: defragment, optimize, shrink, compact, clear or purge the open or selected image (Ctrl+Shift+D)"), this.RibbonAction("Partitions", IconKeys.Create, this.OpenPartitionEditor, RibbonItemSize.Small, Keys.None, "Edit MBR and GPT partition tables"), this.RibbonAction("Mount", IconKeys.Open, this.OpenMountWindow, RibbonItemSize.Small, Keys.None, "Mount a filesystem image")), Group("Performance", @@ -137,13 +140,6 @@ private RibbonTab ArchiveTab() { this.RibbonCommand("Add Files", IconKeys.Add, this._model.AddFilesCommand, RibbonItemSize.Large, Keys.None, "Add files to the archive"), this.RibbonCommand("Test", IconKeys.Test, this._model.TestCommand, RibbonItemSize.Small, Keys.Control | Keys.T, "Test integrity (Ctrl+T)")), Group("Maintenance", - this.RibbonCommand("Compact", IconKeys.Defragment, this._model.CompactEntryCommand, RibbonItemSize.Large, Keys.None, "Defragment, optimize and shrink in one pass"), - this.RibbonCommand("Defragment", IconKeys.Defragment, this._model.DefragmentEntryCommand, RibbonItemSize.Small), - this.RibbonCommand("Optimize", IconKeys.Analyze, this._model.OptimizeEntryCommand, RibbonItemSize.Small), - this.RibbonCommand("Shrink", IconKeys.Defragment, this._model.ShrinkEntryCommand, RibbonItemSize.Small), - this.RibbonCommand("Purge", IconKeys.Remove, this._model.PurgeEntryCommand, RibbonItemSize.Small), - this.RibbonCommand("Wipe Free Space", IconKeys.Remove, this._model.WipeEntryCommand, RibbonItemSize.Small), - this.RibbonCommand("Scramble", IconKeys.Defragment, this._model.ScrambleEntryCommand, RibbonItemSize.Small), this.RibbonCommand("Reconfigure", IconKeys.Properties, this._model.ReconfigureEntryCommand, RibbonItemSize.Small))); return tab; } diff --git a/Compression.NativeUI/Views/MainForm.cs b/Compression.NativeUI/Views/MainForm.cs index 10f33428c..f5d25c13a 100644 --- a/Compression.NativeUI/Views/MainForm.cs +++ b/Compression.NativeUI/Views/MainForm.cs @@ -117,7 +117,8 @@ public MainForm(IEnumerable mountBackends, IMountLaunch this._contentSplit.Panel2.Controls.Add(this._preview); // Added in one place, in z-order: the drop overlay sits above the panes it covers. - this.Controls.AddRange(this._ribbon, this._breadcrumbBar, this._split, this._dropOverlay, this._status); + // The Defragment tab's block view takes the panes' place while that tab is selected. + this.Controls.AddRange(this._ribbon, this._breadcrumbBar, this._split, this._defragView, this._dropOverlay, this._status); this.DragOver += this.OnDragOver; this.DragLeave += (_, _) => this.SetDropOverlay(visible: false, ""); @@ -382,7 +383,7 @@ private void BuildEntryContextMenu() { var maintenance = new ToolStripMenuItem("&Maintenance") { Image = Images.Icon(IconKeys.Defragment) }; maintenance.DropDownItems.AddRange([ Tip(this.Item("&Compact (defrag + optimize + shrink)...", IconKeys.Defragment, Keys.None, this._model.CompactEntryCommand), - "One pass to the smallest valid container holding the same contents. Tick 'Minimal geometry' in the window for the bare-minimum (non-standard) rebuild."), + "One pass to the smallest valid container holding the same contents. Minimal Geometry on the Defragment tab gives the bare-minimum (non-standard) rebuild."), Tip(this.Item("&Reconfigure (geometry/options)...", IconKeys.Properties, Keys.None, this._model.ReconfigureEntryCommand), "Change an existing image's geometry/options after creation (e.g. FAT cluster size or root entries, NTFS MFT record size). Contents preserved byte-for-byte."), new ToolStripSeparator(), @@ -395,7 +396,7 @@ private void BuildEntryContextMenu() { new ToolStripSeparator(), Tip(this.Item("&Purge...", IconKeys.Remove, Keys.None, this._model.PurgeEntryCommand), "Erase ALL live data, leaving a valid empty container."), - Tip(this.Item("&Wipe Empty...", IconKeys.Remove, Keys.None, this._model.WipeEntryCommand), + Tip(this.Item("C&lear Free Space...", IconKeys.Remove, Keys.None, this._model.WipeEntryCommand), "Overwrite only unused space (free clusters, slack, deleted entries). Live data untouched."), Tip(this.Item("Scra&mble...", IconKeys.Defragment, Keys.None, this._model.ScrambleEntryCommand), "Scatter every block of every file across the volume — fragmentation on purpose, so Defragment has something real to work against. Content preserved; asks first."), @@ -463,6 +464,8 @@ private void LayoutChildren() { var contentWidth = width - this._split.SplitterDistance - this._split.SplitterWidth; this._contentSplit.SplitterDistance = Math.Max(this._contentSplit.Panel1MinSize, contentWidth - PreviewPaneWidth); } + this._defragView.Bounds = this._split.Bounds; + this._defragView.SizeFilesPanelOnce(); this._dropOverlay.Bounds = this._split.Bounds; this._status.Bounds = new(0, this.ClientSize.Height - StatusHeight, width, StatusHeight); } @@ -699,19 +702,6 @@ private void SetDropOverlay(bool visible, string message) { // ── Window commands ───────────────────────────────────────────────────────────────────────── - private void OpenMaintenance() { - var preselected = this._model.HasArchive ? this._model.ArchivePath : null; - var window = preselected is not null ? new DefragmentWindow(preselected) : new DefragmentWindow(); - - // Re-list whenever the maintenance window mutates the archive we have open. - window.ArchiveMutated += path => { - if (this._model.HasArchive && string.Equals(path, this._model.ArchivePath, StringComparison.OrdinalIgnoreCase)) - this._model.ReloadArchiveInPlace(); - }; - - window.Show(); - } - private void OpenPartitionEditor() { var preselected = this._model.HasArchive ? this._model.ArchivePath : null; if (preselected is not null && File.Exists(preselected)) { diff --git a/Compression.Registry/DefragRebuilder.cs b/Compression.Registry/DefragRebuilder.cs index 3f8bf4ca3..db00bdfe8 100644 --- a/Compression.Registry/DefragRebuilder.cs +++ b/Compression.Registry/DefragRebuilder.cs @@ -176,8 +176,8 @@ public static void Rebuild( /// classification emitted here is a listing-order proxy only. Honest /// mtime-based classification requires the pre-defrag snapshot built from /// on the descriptor's - /// output (see the WPF - /// DefragmentWindow.PreviewBlockMap method for the canonical + /// output (see the shell's + /// BlockMapSnapshot.Read method for the canonical /// implementation). /// private static DefragProgressEvent BuildScanEvent( @@ -210,7 +210,7 @@ private static DefragProgressEvent BuildScanEvent( /// access to entry mtimes (its input is just (name, bytes) tuples), so this /// proxy is the best the rebuild path can do; UI consumers wanting honest /// mtime-based classification should use the pre-defrag snapshot path - /// (DefragmentWindow.PreviewBlockMap) which has access to + /// (BlockMapSnapshot.Read in the shell) which has access to /// . /// /// diff --git a/Compression.Tests/NativeUI/HeadlessBackend.cs b/Compression.Tests/NativeUI/HeadlessBackend.cs index 980520013..e45331903 100644 --- a/Compression.Tests/NativeUI/HeadlessBackend.cs +++ b/Compression.Tests/NativeUI/HeadlessBackend.cs @@ -81,7 +81,7 @@ public Size MeasureText(string text, Font font) { // rather than a file of empty overrides each is a proxy that answers every call with default. public IButtonPeer CreateButton() => Stub(); public ICheckBoxPeer CreateCheckBox() => Stub(); - public IComboBoxPeer CreateComboBox() => Stub(); + public IComboBoxPeer CreateComboBox() => new RecordingComboBoxPeer(); public IGroupBoxPeer CreateGroupBox() => Stub(); public ILabelPeer CreateLabel() => Stub(); public ILinkLabelPeer CreateLinkLabel() => Stub(); diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs new file mode 100644 index 000000000..5f2fe7d53 --- /dev/null +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -0,0 +1,307 @@ +using System; +using System.IO; +using System.Linq; +using Compression.Core.Layout; +using Compression.Lib; +using Compression.NativeUI; +using Compression.NativeUI.Controls; +using Compression.NativeUI.Navigation; +using Compression.NativeUI.ViewModels; +using Compression.NativeUI.Views; +using Hawkynt.NativeForms; +using NUnit.Framework; + +namespace Compression.Tests.NativeUI; + +/// The Defragment tab, driven through the assembled shell against real images. +public sealed partial class MainFormTests { + private static readonly TimeSpan Patience = TimeSpan.FromMinutes(2); + + /// A scratch folder holding the maintenance fixtures, removed afterwards. + private static void WithImages(Action body) { + var root = MaintenanceFixtures.Scratch(); + UserSettings.PathOverride = Path.Combine(root, "settings.json"); + FormatRegistration.EnsureInitialized(); + try { + body(root); + } finally { + UserSettings.PathOverride = null; + try { Directory.Delete(root, recursive: true); } catch { } + } + } + + private static RibbonContextualTabGroup DiskTools(MainForm shell) => Field(shell, "_diskTools"); + private static RibbonTab DefragTab(MainForm shell) => Field(shell, "_defragTab"); + private static DefragmentView DefragView(MainForm shell) => Field(shell, "_defragView"); + private static T Item(MainForm shell, string text) where T : RibbonItem + => DefragTab(shell).Groups.SelectMany(g => g.Items.Cast()).OfType().Single(i => i.Text == text); + private static RibbonToggleButton Toggle(MainForm shell, string text) => Item(shell, text); + + /// Opens , selects the Defragment tab and waits for the first map. + private static void OpenOnDefragmentTab(MainForm shell, string image) { + shell.OpenArchive(image); + RibbonItemNamed(shell, "Maintenance").PerformClick(); + Assert.That(Field(shell, "_ribbon").SelectedTab, Is.SameAs(DefragTab(shell)), "Maintenance opens the Defragment tab"); + Settle(shell); + } + + /// Waits for whatever the tab started — an operation, then the analysis it triggers. + private static void Settle(MainForm shell) { + Assert.That(shell.MaintenanceTask?.Wait(Patience) ?? true, Is.True, "the operation finished"); + Assert.That(shell.AnalysisTask?.Wait(Patience) ?? true, Is.True, "the analysis finished"); + } + + // ── the tab comes and goes with a target ──────────────────────────────────────────────────── + + [Test] + public void GivenNoImage_WhenTheShellStarts_ThenTheDiskToolsTabIsHiddenAndMaintenanceIsDisabled() { + WithImages(_ => WithShell(shell => { + Assert.That(DiskTools(shell).Visible, Is.False); + Assert.That(RibbonItemNamed(shell, "Maintenance").Enabled, Is.False, "nothing to maintain"); + })); + } + + [Test] + public void GivenAFatImage_WhenOpenedAndThenLeftForAFolder_ThenTheTabAppearsAndGoes() { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + + shell.OpenArchive(MaintenanceFixtures.FragmentedFat(root)); + Assert.That(DiskTools(shell).Visible, Is.True, "an open FAT image can be maintained"); + + model.NavigateTo(Location.Folder(Path.Combine(root))); + model.SelectedEntries.Clear(); + CommandManager.InvalidateRequerySuggested(); + Assert.That(DiskTools(shell).Visible, Is.False, "a folder with nothing selected has nothing to maintain"); + })); + } + + [Test] + public void GivenAPartitionedDisk_WhenOpened_ThenNoMaintenanceIsOffered() { + WithImages(root => WithShell(shell => { + shell.OpenArchive(MaintenanceFixtures.PartitionedDisk(root)); + + Assert.That(Field(shell, "_model").Format, Is.EqualTo("PartitionedDisk")); + Assert.That(DiskTools(shell).Visible, Is.False, "a partitioned disk is read-only as a whole"); + Assert.That(RibbonItemNamed(shell, "Maintenance").Enabled, Is.False); + })); + } + + // ── the client area ───────────────────────────────────────────────────────────────────────── + + [Test] + public void GivenTheBrowser_WhenTheDefragmentTabIsEnteredAndLeft_ThenTheBlockViewTakesThePanesPlaceAndTheBrowserReturnsAsItWas() { + WithImages(root => WithShell(shell => { + var split = Field(shell, "_split"); + var ribbon = Field(shell, "_ribbon"); + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.FragmentedFat(root)); + split.Panel1Collapsed = true; + var (distance, location, bounds) = (split.SplitterDistance, model.CurrentLocation, split.Bounds); + + OpenOnDefragmentTab(shell, model.ArchivePath); + var view = DefragView(shell); + Assert.Multiple(() => { + Assert.That(split.Visible, Is.False, "the browser gives up the client area"); + Assert.That(view.Visible, Is.True); + Assert.That(view.Bounds, Is.EqualTo(bounds), "the block view gets the whole client area the panes had"); + Assert.That(view.Map.BlockMap, Is.Not.Null.And.Not.Empty, "the map is drawn on entry"); + }); + + ribbon.SelectedIndex = 1; + Assert.Multiple(() => { + Assert.That(view.Visible, Is.False); + Assert.That(split.Visible, Is.True); + Assert.That(split.Panel1Collapsed, Is.True, "a folded navigation pane stays folded"); + Assert.That(split.SplitterDistance, Is.EqualTo(distance)); + Assert.That(model.CurrentLocation, Is.EqualTo(location), "the browser is where it was left"); + }); + })); + } + + // ── what is offered ───────────────────────────────────────────────────────────────────────── + + [Test] + public void GivenAFatImage_WhenOnTheTab_ThenDefragmentIsOfferedAndOptimizeIsNotAndEachSaysWhy() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + + Assert.Multiple(() => { + Assert.That(Toggle(shell, "Defragment").Enabled, Is.True); + Assert.That(Toggle(shell, "Defragment").Checked, Is.True, "the first supported operation is preselected"); + Assert.That(Toggle(shell, "Shrink").Enabled, Is.True); + Assert.That(Toggle(shell, "Clear").Enabled, Is.True); + Assert.That(Toggle(shell, "Optimize").Enabled, Is.False); + Assert.That(Toggle(shell, "Optimize").ToolTipText, Does.Contain("no optimizer")); + Assert.That(Toggle(shell, "Sort Entries").Enabled, Is.False); + Assert.That(Toggle(shell, "Sort Entries").ToolTipText, Does.Contain("sorting directory entries")); + Assert.That(Item(shell, "Start").Enabled, Is.True); + }); + })); + } + + [Test] + public void GivenAZip_WhenOnTheTab_ThenDefragmentIsNotOfferedButOptimizeAndPurgeAre() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.Zip(root)); + + Assert.Multiple(() => { + Assert.That(Toggle(shell, "Defragment").Enabled, Is.False); + Assert.That(Toggle(shell, "Optimize").Enabled, Is.True); + Assert.That(Toggle(shell, "Optimize").Checked, Is.True); + Assert.That(Toggle(shell, "Purge").Enabled, Is.True); + Assert.That(Toggle(shell, "Consolidate").Enabled, Is.False, "no defrag mode applies to an archive"); + }); + })); + } + + [Test] + public void GivenDefragment_WhenAnotherOperationIsPicked_ThenTheDefragModesAndLayoutOptionsGreyOut() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + var interleave = Field(shell, "_interleaveItem"); + Assert.That((Toggle(shell, "Consolidate").Enabled, interleave.Enabled), Is.EqualTo((true, true))); + + Toggle(shell, "Shrink").PerformClick(); + + Assert.Multiple(() => { + Assert.That(Toggle(shell, "Shrink").Checked, Is.True); + Assert.That(Toggle(shell, "Defragment").Checked, Is.False, "operations are a radio group"); + foreach (var mode in new[] { "Consolidate", "Defrag", "Re-order", "Carve Hole" }) { + Assert.That(Toggle(shell, mode).Enabled, Is.False, mode); + Assert.That(Toggle(shell, mode).ToolTipText, Does.Contain("Applies to Defragment"), mode); + } + Assert.That(interleave.Enabled, Is.False); + }); + + Toggle(shell, "Shrink").PerformClick(); + Assert.That(Toggle(shell, "Shrink").Checked, Is.True, "clicking the picked operation keeps it picked"); + })); + } + + [Test] + public void GivenCarveHole_WhenPickedAndUnpicked_ThenItsSizeAndOffsetAppearOnlyWhilePicked() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + var size = Field(shell, "_holeSizeItem"); + var at = Field(shell, "_holeAtItem"); + Assert.That((size.Visible, at.Visible), Is.EqualTo((false, false))); + + Toggle(shell, "Carve Hole").PerformClick(); + Assert.That((size.Visible, at.Visible), Is.EqualTo((true, true))); + + Toggle(shell, "Re-order").PerformClick(); + Assert.That((size.Visible, at.Visible), Is.EqualTo((false, false))); + })); + } + + [TestCase("0", false)] + [TestCase("1", true)] + [TestCase("256", true)] + [TestCase("257", false)] + [TestCase("two", false)] + public void GivenAnInterleave_WhenTyped_ThenStartFollowsWhetherItIsInRange(string stride, bool startable) { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + + Field(shell, "_interleaveBox").Text = stride; + + var start = Item(shell, "Start"); + Assert.That(start.Enabled, Is.EqualTo(startable)); + if (!startable) Assert.That(start.ToolTipText, Does.Contain("Interleave"), "Start says what blocks it"); + })); + } + + [Test] + public void GivenTheViewToggles_WhenEachIsClicked_ThenTheBlockMapSwitchesAndTheOthersRelease() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + var map = DefragView(shell).Map; + + foreach (var (text, view) in new[] { ("Circle", BlockMapView.CircularPlatter), ("3D Stack", BlockMapView.CylinderStack), ("Blocks", BlockMapView.LinearBlocks) }) { + Toggle(shell, text).PerformClick(); + Assert.That(map.ViewMode, Is.EqualTo(view), text); + Assert.That(new[] { "Blocks", "Circle", "3D Stack" }.Count(t => Toggle(shell, t).Checked), Is.EqualTo(1), text); + } + + Toggle(shell, "Files").PerformClick(); + Assert.That(DefragView(shell).FilesPanelVisible, Is.False); + })); + } + + /// + /// The view is hidden while the ribbon is built, so asking a child whether it is visible answers + /// no. Syncing the toggles from that answer once switched the colour key off for good. + /// + [Test] + public void GivenTheDefragmentTab_WhenEntered_ThenTheColourKeyIsShownAndItsToggleHidesIt() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + var legend = Field(DefragView(shell), "_legend"); + + Assert.That((Toggle(shell, "Legend").Checked, legend.Visible), Is.EqualTo((true, true))); + + Toggle(shell, "Legend").PerformClick(); + Assert.That((Toggle(shell, "Legend").Checked, legend.Visible), Is.EqualTo((false, false))); + })); + } + + [Test] + public void GivenAContextMenuVerb_WhenRun_ThenTheDefragmentTabOpensWithThatOperationPicked() { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.FragmentedFat(root)); + + model.ShrinkEntryCommand.Execute(null); + Settle(shell); + + Assert.That(Field(shell, "_ribbon").SelectedTab, Is.SameAs(DefragTab(shell))); + Assert.That(Toggle(shell, "Shrink").Checked, Is.True); + })); + } + + // ── running ───────────────────────────────────────────────────────────────────────────────── + + [Test] + public void GivenAFragmentedFat_WhenStartIsClicked_ThenItIsDefragmentedInPlaceWithSizeAndContentsUnchanged() { + WithImages(root => WithShell(shell => { + var image = MaintenanceFixtures.FragmentedFat(root); + var size = new FileInfo(image).Length; + OpenOnDefragmentTab(shell, image); + Assume.That(DefragView(shell).StatusText, Does.Contain("fragmented file")); + + Item(shell, "Start").PerformClick(); + Settle(shell); + + var model = Field(shell, "_model"); + Assert.Multiple(() => { + Assert.That(model.StatusText, Does.StartWith("Defragment finished"), model.StatusText); + Assert.That(new FileInfo(image).Length, Is.EqualTo(size), "total size unchanged"); + var after = MaintenanceFixtures.ReadFatFiles(image); + foreach (var (name, data) in MaintenanceFixtures.FatFiles) + Assert.That(after[name], Is.EqualTo(data), $"{name} byte-identical"); + Assert.That(DefragView(shell).StatusText, Does.Contain("no fragmentation"), "the map is read again afterwards"); + Assert.That(DiskTools(shell).Visible, Is.True, "the tab stays with its image after the reload"); + }); + })); + } + + [Test] + public void GivenARequestTheFormatRefuses_WhenStarted_ThenItIsReportedAndTheImageIsByteIdentical() { + WithImages(root => WithShell(shell => { + var image = MaintenanceFixtures.FragmentedFat(root); + var before = File.ReadAllBytes(image); + OpenOnDefragmentTab(shell, image); + + Toggle(shell, "Carve Hole").PerformClick(); + Field(shell, "_holeSizeBox").Text = "1g"; + Item(shell, "Start").PerformClick(); + Settle(shell); + + Assert.Multiple(() => { + Assert.That(Field(shell, "_model").StatusText, Does.Contain("refused").And.Contain("unchanged")); + Assert.That(File.ReadAllBytes(image), Is.EqualTo(before)); + }); + })); + } +} diff --git a/Compression.Tests/NativeUI/MainFormTests.cs b/Compression.Tests/NativeUI/MainFormTests.cs index 1204abc12..648d26e2a 100644 --- a/Compression.Tests/NativeUI/MainFormTests.cs +++ b/Compression.Tests/NativeUI/MainFormTests.cs @@ -28,7 +28,7 @@ namespace Compression.Tests.NativeUI; /// [TestFixture] [NonParallelizable] -public sealed class MainFormTests { +public sealed partial class MainFormTests { private const BindingFlags Private = BindingFlags.Instance | BindingFlags.NonPublic; /// @@ -207,6 +207,7 @@ public void GivenViewAsText_WhenTheRibbonIsBuilt_ThenEnterIsShownButNotRegistere [TestCase("New Folder", Keys.Control | Keys.Shift | Keys.N)] [TestCase("Select All", Keys.Control | Keys.A)] [TestCase("Preview Pane", Keys.Alt | Keys.P)] + [TestCase("Maintenance", Keys.Control | Keys.Shift | Keys.D)] public void GivenACommand_WhenTheRibbonIsBuilt_ThenItCarriesItsShortcut(string label, Keys expected) { WithShell(shell => { Assert.That(RibbonItemNamed(shell, label).ShortcutKeys, Is.EqualTo(expected)); diff --git a/Compression.Tests/NativeUI/MaintenanceFixtures.cs b/Compression.Tests/NativeUI/MaintenanceFixtures.cs new file mode 100644 index 000000000..348a12a4d --- /dev/null +++ b/Compression.Tests/NativeUI/MaintenanceFixtures.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using Compression.Core.DiskImage; +using Compression.Lib; +using Compression.Registry; + +namespace Compression.Tests.NativeUI; + +/// Real images for the Defragment tab tests: a fragmented FAT floppy, a ZIP and a partitioned disk. +internal static class MaintenanceFixtures { + /// The files on the FAT fixture, by name, so a test can prove each survived byte for byte. + public static readonly IReadOnlyDictionary FatFiles = new Dictionary { + ["ALPHA.BIN"] = Pattern(23_000, 0x11), + ["BRAVO.BIN"] = Pattern(41_500, 0x22), + ["CHARLIE.TXT"] = Pattern(9_100, 0x33), + ["DELTA.DAT"] = Pattern(57_000, 0x44), + ["ECHO.LOG"] = Pattern(3_300, 0x55), + }; + + /// A 1.44 MB FAT12 floppy whose files are scattered on purpose, so defragmenting moves something. + public static string FragmentedFat(string folder, string name = "volume.img") { + var writer = new FileSystem.Fat.FatWriter(); + foreach (var (file, data) in FatFiles) writer.AddFile(file, data); + var path = Path.Combine(folder, name); + File.WriteAllBytes(path, writer.Build()); + + FormatRegistration.EnsureInitialized(); + using var image = File.Open(path, FileMode.Open, FileAccess.ReadWrite); + ((IFilesystemScrambleable)FormatRegistry.GetArchiveOps("Fat")!).Scramble(image, new ScrambleOptions { Seed = 0x5EED }); + return path; + } + + public static string Zip(string folder, string name = "bundle.zip") { + var path = Path.Combine(folder, name); + using var archive = ZipFile.Open(path, ZipArchiveMode.Create); + using (var writer = new StreamWriter(archive.CreateEntry("readme.txt").Open())) writer.Write(new string('z', 4000)); + using (var writer = new StreamWriter(archive.CreateEntry("docs/notes.txt").Open())) writer.Write("notes"); + return path; + } + + /// A whole-disk image: an MBR with one FAT partition starting at 1 MiB. + public static string PartitionedDisk(string folder, string name = "disk.img") { + const long mib = 1024 * 1024; + var writer = new FileSystem.Fat.FatWriter(); + writer.AddFile("HELLO.TXT", "hello"u8.ToArray()); + var fat = writer.Build(); + var length = (fat.Length + 511) / 512 * 512; + var disk = new byte[mib + length + mib]; + using (var stream = new MemoryStream(disk, writable: true)) + new PartitionEditor(stream).AddPartition(mib, length, PartitionType.Fat16Lba, "data"); + fat.CopyTo(disk, (int)mib); + + var path = Path.Combine(folder, name); + File.WriteAllBytes(path, disk); + return path; + } + + /// Every file on the FAT image, read back through the descriptor. + public static Dictionary ReadFatFiles(string path) { + var ops = FormatRegistry.GetArchiveOps("Fat")!; + using var stream = File.OpenRead(path); + var names = ops.List(stream, password: null).Where(e => !e.IsDirectory).Select(e => e.Name).ToList(); + return names.ToDictionary(n => n, n => ArchiveOperations.ExtractEntry(path, n, password: null)); + } + + public static string Scratch() { + var root = Path.Combine(Path.GetTempPath(), "cwb-defrag-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + return root; + } + + private static byte[] Pattern(int length, int seed) { + var data = new byte[length]; + for (var i = 0; i < length; ++i) data[i] = (byte)((i * 31 + seed + i / 251) & 0xFF); + return data; + } +} diff --git a/Compression.Tests/NativeUI/MaintenancePresenterTests.cs b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs new file mode 100644 index 000000000..cb20b342f --- /dev/null +++ b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs @@ -0,0 +1,228 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using Compression.Lib; +using Compression.NativeUI.Maintenance; +using Compression.Registry; +using NUnit.Framework; + +namespace Compression.Tests.NativeUI; + +/// +/// The Defragment tab's logic without the tab: what each real format is said to support, how typed +/// values are read, how the choices map onto the engine, and that a run keeps the defrag invariants +/// or refuses with the image untouched. +/// +[TestFixture] +public sealed class MaintenancePresenterTests { + private string _root = null!; + + [SetUp] + public void SetUp() { + FormatRegistration.EnsureInitialized(); + this._root = MaintenanceFixtures.Scratch(); + } + + [TearDown] + public void TearDown() { + try { Directory.Delete(this._root, recursive: true); } catch { } + } + + // ── capabilities ──────────────────────────────────────────────────────────────────────────── + + [Test] + public void GivenFat_WhenCapabilitiesAreAsked_ThenDefragmentAndItsInPlaceModesAreOffered() { + var caps = MaintenanceCapabilities.For("Fat"); + + Assert.Multiple(() => { + Assert.That(caps.Verb(MaintenanceVerb.Defragment).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.Shrink).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.WipeEmpty).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.Scramble).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.Optimize).Supported, Is.False, "a filesystem's layout operation is Defragment; Optimize would copy it through"); + foreach (var strategy in new[] { DefragStrategy.Consolidate, DefragStrategy.Defrag, DefragStrategy.Reorder, DefragStrategy.CarveHole }) + Assert.That(caps.Strategy(strategy).Supported, Is.True, strategy.ToString()); + Assert.That(caps.PackAtStart.Supported && caps.PackAtEnd.Supported, Is.True); + Assert.That(caps.Interleave.Supported, Is.True, "FAT's planner honours block interleave"); + Assert.That(caps.MetadataZone.Supported, Is.True); + Assert.That(caps.LayoutProfile.Supported, Is.False, "FAT refuses layout templates, so the profile is not offered"); + }); + } + + [Test] + public void GivenAnyFormat_WhenSortEntriesIsAsked_ThenItIsRefusedWithAReason() { + foreach (var format in new[] { "Fat", "Zip", "Ntfs", "ExFat" }) { + var sort = MaintenanceCapabilities.For(format).Strategy(DefragStrategy.SortEntries); + Assert.That(sort.Supported, Is.False, format); + Assert.That(sort.Reason, Does.Contain("sorting directory entries"), format); + } + } + + [Test] + public void GivenZip_WhenCapabilitiesAreAsked_ThenDefragmentIsNotOfferedButOptimizePurgeAndClearAre() { + var caps = MaintenanceCapabilities.For("Zip"); + + Assert.Multiple(() => { + Assert.That(caps.Verb(MaintenanceVerb.Defragment).Supported, Is.False, "an archive has no free-space layout to defragment (#432)"); + Assert.That(caps.Verb(MaintenanceVerb.Optimize).Supported, Is.True); + Assert.That(caps.OptimizeRoute, Is.EqualTo(OptimizeRoute.Reencode)); + Assert.That(caps.Verb(MaintenanceVerb.Purge).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.WipeEmpty).Supported, Is.True); + Assert.That(caps.Verb(MaintenanceVerb.Scramble).Supported, Is.False); + Assert.That(Enum.GetValues().Any(s => caps.Strategy(s).Supported), Is.False); + }); + } + + [Test] + public void GivenAPartitionedDisk_WhenCapabilitiesAreAsked_ThenNothingIsOffered() { + var caps = MaintenanceCapabilities.For("PartitionedDisk"); + + Assert.That(caps.AnySupported, Is.False, "a partitioned disk is read-only; each volume is maintained on its own"); + Assert.That(caps.Verbs.Values.All(v => v.Reason.Length > 0), Is.True, "every refusal says why"); + } + + // ── typed values ──────────────────────────────────────────────────────────────────────────── + + [TestCase("0", false)] + [TestCase("1", true)] + [TestCase("2", true)] + [TestCase("256", true)] + [TestCase("257", false)] + [TestCase("-1", false)] + [TestCase("abc", false)] + [TestCase("1.5", false)] + [TestCase("", false)] + [TestCase(" 16 ", true)] + public void GivenAnInterleave_WhenParsed_ThenOnlyOneTo256IsAccepted(string text, bool valid) { + var parsed = MaintenanceInput.ParseInterleave(text); + + Assert.That(parsed.IsValid, Is.EqualTo(valid), parsed.Error); + if (valid) Assert.That(parsed.Value, Is.EqualTo(int.Parse(text.Trim()))); + } + + [TestCase("524288", 524288L)] + [TestCase("64k", 64L * 1024)] + [TestCase("64m", 64L * 1024 * 1024)] + [TestCase("1g", 1L << 30)] + [TestCase("1G", 1L << 30)] + public void GivenAHoleSize_WhenParsed_ThenSuffixesScale(string text, long expected) + => Assert.That(MaintenanceInput.ParseSize(text).Value, Is.EqualTo(expected)); + + [TestCase("0")] + [TestCase("")] + [TestCase("m")] + [TestCase("-5")] + [TestCase("ten")] + [TestCase("99999999999999999g")] + public void GivenAnUnusableHoleSize_WhenParsed_ThenItIsRefused(string text) + => Assert.That(MaintenanceInput.ParseSize(text).IsValid, Is.False); + + [TestCase("auto", -1L, true)] + [TestCase("", -1L, true)] + [TestCase("4096", 4096L, true)] + [TestCase("-4096", 0L, false)] + [TestCase("start", 0L, false)] + public void GivenAHoleOffset_WhenParsed_ThenAutoOrAnOffsetIsAccepted(string text, long expected, bool valid) { + var parsed = MaintenanceInput.ParseHoleAt(text); + Assert.That(parsed.IsValid, Is.EqualTo(valid)); + if (valid) Assert.That(parsed.Value, Is.EqualTo(expected)); + } + + // ── choices onto the engine ───────────────────────────────────────────────────────────────── + + [TestCase("Consolidate", false, DefragMode.ConsolidateAtStart)] + [TestCase("Consolidate", true, DefragMode.ConsolidateAtEnd)] + [TestCase("Defrag", false, DefragMode.FillHolesLazy)] + [TestCase("Reorder", false, DefragMode.AscendingOrder)] + [TestCase("CarveHole", false, DefragMode.CarveHole)] + public void GivenAStrategy_WhenOptionsAreBuilt_ThenItMapsOntoItsEngineMode(string strategy, bool packAtEnd, DefragMode expected) { + var presenter = new MaintenancePresenter(MaintenanceFixtures.FragmentedFat(this._root), "Fat") { + Verb = MaintenanceVerb.Defragment, Strategy = Enum.Parse(strategy), PackAtEnd = packAtEnd, + }; + + Assert.That(presenter.BuildDefragOptions().Mode, Is.EqualTo(expected)); + } + + [Test] + public void GivenCarveHole_WhenOptionsAreBuilt_ThenTheTypedSizeAndOffsetReachTheEngine() { + var presenter = new MaintenancePresenter(MaintenanceFixtures.FragmentedFat(this._root), "Fat") { + Verb = MaintenanceVerb.Defragment, Strategy = DefragStrategy.CarveHole, HoleSizeText = "64k", HoleAtText = "8192", InterleaveText = "3", + }; + + var options = presenter.BuildDefragOptions(); + Assert.That((options.HoleSize, options.HoleAt, options.InterleaveStride), Is.EqualTo((64L * 1024, 8192L, 3))); + } + + [Test] + public void GivenAnOptionTheFormatRefuses_WhenOptionsAreBuilt_ThenItStaysAtItsDefault() { + var presenter = new MaintenancePresenter(MaintenanceFixtures.FragmentedFat(this._root), "Fat") { + Verb = MaintenanceVerb.Defragment, LayoutProfile = new() { Name = "ignored" }, + }; + + Assert.That(presenter.BuildDefragOptions().LayoutTemplate, Is.Null, "a disabled option must not reach the engine to be refused or ignored"); + } + + [TestCase("0")] + [TestCase("257")] + [TestCase("x")] + public void GivenAnInvalidInterleave_WhenDefragmentIsPicked_ThenStartIsBlockedWithTheReason(string stride) { + var presenter = new MaintenancePresenter(MaintenanceFixtures.FragmentedFat(this._root), "Fat") { Verb = MaintenanceVerb.Defragment, InterleaveText = stride }; + + Assert.That(presenter.StartBlocker, Does.Contain("Interleave")); + } + + [Test] + public void GivenAnInvalidInterleave_WhenAnotherOperationIsPicked_ThenItDoesNotBlockStart() { + var presenter = new MaintenancePresenter(MaintenanceFixtures.FragmentedFat(this._root), "Fat") { Verb = MaintenanceVerb.WipeEmpty, InterleaveText = "0" }; + + Assert.That(presenter.StartBlocker, Is.Null, "interleave only applies to Defragment"); + } + + // ── running ───────────────────────────────────────────────────────────────────────────────── + + [Test] + public void GivenAFragmentedFat_WhenDefragmented_ThenSizeAndEveryFileAreUnchangedAndTheFilesAreContiguous() { + var path = MaintenanceFixtures.FragmentedFat(this._root); + var sizeBefore = new FileInfo(path).Length; + var presenter = new MaintenancePresenter(path, "Fat") { Verb = MaintenanceVerb.Defragment, Strategy = DefragStrategy.Consolidate }; + Assume.That(presenter.Analyze().Status, Does.Contain("fragmented file"), "the fixture must start fragmented"); + + var outcome = presenter.RunAsync(_ => { }, _ => { }, CancellationToken.None).Result; + + Assert.Multiple(() => { + Assert.That(outcome.Kind, Is.EqualTo(MaintenanceOutcomeKind.Succeeded), outcome.Summary); + Assert.That(new FileInfo(path).Length, Is.EqualTo(sizeBefore), "defragmenting never changes the image size"); + var after = MaintenanceFixtures.ReadFatFiles(path); + foreach (var (name, data) in MaintenanceFixtures.FatFiles) + Assert.That(after[name], Is.EqualTo(data), name); + Assert.That(presenter.Analyze().Status, Does.Contain("no fragmentation")); + }); + } + + [Test] + public void GivenAHoleLargerThanTheVolume_WhenCarved_ThenTheRunIsRefusedAndTheImageIsByteIdentical() { + var path = MaintenanceFixtures.FragmentedFat(this._root); + var before = File.ReadAllBytes(path); + var presenter = new MaintenancePresenter(path, "Fat") { Verb = MaintenanceVerb.Defragment, Strategy = DefragStrategy.CarveHole, HoleSizeText = "1g" }; + + var outcome = presenter.RunAsync(_ => { }, _ => { }, CancellationToken.None).Result; + + Assert.Multiple(() => { + Assert.That(outcome.Kind, Is.EqualTo(MaintenanceOutcomeKind.Refused), outcome.Summary); + Assert.That(outcome.Mutated, Is.False); + Assert.That(File.ReadAllBytes(path), Is.EqualTo(before), "a refusal leaves the image exactly as it was"); + }); + } + + [Test] + public void GivenAnUnsupportedOperation_WhenRun_ThenItIsRefusedWithoutTouchingTheImage() { + var path = MaintenanceFixtures.Zip(this._root); + var before = File.ReadAllBytes(path); + var presenter = new MaintenancePresenter(path, "Zip") { Verb = MaintenanceVerb.Defragment }; + + var outcome = presenter.RunAsync(_ => { }, _ => { }, CancellationToken.None).Result; + + Assert.That((outcome.Kind, File.ReadAllBytes(path).SequenceEqual(before)), Is.EqualTo((MaintenanceOutcomeKind.Refused, true))); + } +} diff --git a/Compression.Tests/NativeUI/RecordingPeers.cs b/Compression.Tests/NativeUI/RecordingPeers.cs index 5a4f8beca..39b205906 100644 --- a/Compression.Tests/NativeUI/RecordingPeers.cs +++ b/Compression.Tests/NativeUI/RecordingPeers.cs @@ -175,4 +175,27 @@ public void SetTopMost(bool topMost) { } public void SetWindowState(FormWindowState state) { } } +/// +/// A combo box peer that keeps its items and selection. It has to be a real class: the interface +/// takes its items as a , which a dispatch proxy cannot box. +/// +internal sealed class RecordingComboBoxPeer : RecordingControlPeer, IComboBoxPeer { + private int _selectedIndex = -1; + + public string[] Items { get; private set; } = []; + + public event EventHandler? SelectionChanged; + public event EventHandler? DropDownOpened; + public event EventHandler? DropDownClosed; + + public void SetItems(ReadOnlySpan items, int selectedIndex) { + this.Items = items.ToArray(); + this._selectedIndex = selectedIndex; + } + + public void SetSelectedIndex(int index) => this._selectedIndex = index; + public int GetSelectedIndex() => this._selectedIndex; + public void SetDroppedDown(bool droppedDown) { } +} + #pragma warning restore CS0067 From 5aa5039561830151a8f07041c4670db829341076 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 07:14:43 +0200 Subject: [PATCH 02/16] * the READMEs describe the Defragment tab instead of the maintenance window --- Compression.NativeUI/README.md | 29 ++++++++++++++++++++++++----- README.md | 11 +++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Compression.NativeUI/README.md b/Compression.NativeUI/README.md index e5fc2bcfd..bd494e027 100644 --- a/Compression.NativeUI/README.md +++ b/Compression.NativeUI/README.md @@ -8,8 +8,9 @@ backends, so Windows and Linux run the same code rather than two frontends drift - **Ribbon** — the commands live in an Office-style ribbon instead of a menu bar: a Quick Access Toolbar (Back, Forward, Up, Refresh), File, Home (clipboard, organize, open, selection), View (navigation and preview panes, Details / Thumbnails), Tools (analysis, maintenance, partitions, - mounting, benchmark), and an Archive Tools tab that appears only inside an archive (extract, add, - test, and the maintenance verbs). Every shortcut works form-wide from the ribbon. + mounting, benchmark), an Archive Tools tab that appears only inside an archive (extract, add, test, + reconfigure), and a Disk Tools tab, Defragment, that appears while there is something to maintain. + Every shortcut works form-wide from the ribbon. - **Archive browser** — the main window, laid out like a file manager. A folder tree on the left shows the drives (or `/`) and home, with the open archive grafted in as a folder beside the file it lives in; it follows every navigation, however it happened. The breadcrumb bar is also an address @@ -49,8 +50,26 @@ backends, so Windows and Linux run the same code rather than two frontends drift for a directory. - **Binary analysis** — magic scan, algorithm fingerprints, entropy map, heatmap, trial decompression, chain reconstruction, statistics, strings and a struct-template interpreter. -- **Maintenance** — defragment, optimize, shrink, purge, wipe-empty, compact and scramble, over a - live block map that can also be projected onto a platter or a stack of platters. +- **Defragment tab** — maintenance is a contextual ribbon tab rather than a window. It appears while + the open archive or image, or an archive file selected in a folder, supports any maintenance + operation (Tools → Maintenance or Ctrl+Shift+D selects it; the Maintenance entries of the list's + context menu open it with their operation picked). Selecting it hands the client area below the + breadcrumb bar to the block map; leaving it shows the browser again exactly as it was. + + | Group | Items | + | --- | --- | + | Operation | Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Start; Stop where the operation can be stopped | + | Defrag Mode | Consolidate (Pack at End as its variant), Defrag (fill holes), Re-order (ascending blocks), Sort Entries, Carve Hole with its size and offset fields | + | Options | Block interleave (1–256), metadata placement, layout profile and Edit Profiles; the seed for Scramble, the chunk placement for a file-internal Optimize and Minimal Geometry for Compact appear while their operation is picked | + | View | Blocks, Circle, 3D Stack; Files panel; Legend; Analyze (read the layout again) | + + Every item asks one capability adapter (`Maintenance/MaintenanceCapabilities.cs`) whether the target + supports it, and is disabled with the reason in its tooltip when not — Sort Entries, for instance, + says that no format offers it yet. Defrag modes and layout options are probed against the + descriptor's own refusal guard without touching the image. Operations run off the UI thread through + `MaintenancePresenter`, driving the map live; a refusal (`NotSupportedException`) is reported and + leaves the image byte-identical, and after a change the archive is re-listed in place. A nested + archive is maintained as a temporary copy and written back into its host after each change. - **Partition editor** — MBR and GPT tables: add, delete, purge, convert, format, verify. - **Benchmark** — every building block against seven synthetic data patterns. - **Reverse engineer** — discovers an unknown format by probing a tool or by locating known content @@ -85,7 +104,7 @@ ran at a scale factor other than 1. ## Screenshots `--screenshot=` builds a deterministic fixture and shows that -one window. It does not write an image: NativeForms has no way to render a window to a bitmap, so CI +one window (`maintenance` is the shell on the Defragment tab over a scrambled FAT floppy). It does not write an image: NativeForms has no way to render a window to a bitmap, so CI takes the picture from outside with an X11 capture tool. Everything that makes a capture reproducible lives here — fixed payloads, fixed timestamps, a fixed scramble seed, and an analysis window that leaves its elapsed time out of the status line. diff --git a/README.md b/README.md index 4889577a9..561917aae 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,17 @@ Power-user navigation includes: - **Registered file-type filters** — the Open dialog exposes an all-formats filter plus generated per-format filters. +**Maintenance** has its own contextual ribbon tab, *Disk Tools → Defragment* (Tools → Maintenance, +Ctrl+Shift+D), shown while an image or archive that supports any maintenance operation is open or +selected. While it is selected the block map takes the whole client area — linear blocks, a circular +platter or a 3-D stack of platters, with an optional files panel and colour key — and the ribbon picks +the operation (Defragment, Optimize, Shrink, Compact, Clear free space, Purge, Scramble), the +defragmentation mode (Consolidate at the start or end, Defrag by filling holes, Re-order into ascending +blocks, Carve a hole of a chosen size and offset), the block interleave (1–256), metadata placement and +layout profile. Each item is enabled only where the format supports it and says why when it is not; +a request the format refuses leaves the image byte-identical. Leaving the tab returns the browser +exactly as it was. + The Binary Analysis wizard walks progressively deeper through an unknown binary: - **Scan Results** — registered magic-byte signatures with offsets and confidence. From 9a0f36fe183b5231a88221d21a21e9ec8a066e15 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 07:23:24 +0200 Subject: [PATCH 03/16] * the metadata placement choices fit the narrow ribbon field On GTK the field shows the end of a long entry, so "Don't move metadata" read as "metadata". The choices are one or two words now; the tooltip explains each. --- Compression.NativeUI/Views/MainForm.Defragment.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index c11658e1e..4c1877ea7 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -153,20 +153,15 @@ private RibbonGroup StrategyGroup() { /// that applies keeps the tab narrow enough that no group with a hosted field has to fold away. /// private RibbonGroup OptionsGroup() { - this._metadataZoneCombo.Items.AddRange([ - "Don't move metadata", - "Metadata at front", - "Metadata at back", - "Metadata in middle", - "Before file content", - ]); + // Short, because the field is narrow; the tooltip says what each placement means. + this._metadataZoneCombo.Items.AddRange(["Unchanged", "Front", "Back", "Middle", "Before data"]); this._metadataZoneCombo.SelectedIndex = 0; this.RefreshLayoutProfiles(); this._interleaveItem = this.Field("Interleave", this._interleaveBox, $"Block interleave: 1 = contiguous, 2 = every other block, N = each file's Kth block at start + K×N. {MaintenanceInput.MinInterleave}–{MaintenanceInput.MaxInterleave}.", () => this.WithPresenter(p => p.InterleaveText = this._interleaveBox.Text)); - this._metadataZoneItem = this.Field("Metadata", this._metadataZoneCombo, "Where filesystem metadata and directory entries are placed.", + this._metadataZoneItem = this.Field("Metadata", this._metadataZoneCombo, "Where filesystem metadata and directory entries are placed: unchanged, at the front (fast access), at the back (data first), in the middle (least seeking) or just before the file content (read-ahead).", () => this.WithPresenter(p => p.MetadataZone = this.SelectedMetadataZone())); this._layoutProfileItem = this.Field("Profile", this._layoutProfileCombo, "A zone-based layout template: files go into named byte ranges with per-zone sort orders.", @@ -176,7 +171,7 @@ private RibbonGroup OptionsGroup() { this.RefreshLayoutProfiles(); }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); - this._placementCombo.Items.AddRange(["Format default", "Metadata first", "Data first"]); + this._placementCombo.Items.AddRange(["Default", "Metadata first", "Data first"]); this._placementCombo.SelectedIndex = 0; this._seedItem = this.Field("Seed", this._seedBox, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", From 0f30e84ee01081452107a7c696807bebf4678c5b Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 07:32:24 +0200 Subject: [PATCH 04/16] * the Defragment tab's fields are wide enough for a GTK combo box GTK draws a combo box at its natural width, wider than the 88 px the field gave it, so it spilled under its caption and showed "nchanged". The input part is 116 px now. --- Compression.NativeUI/Views/MainForm.Defragment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index 4c1877ea7..8276a0ed7 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -25,7 +25,7 @@ namespace Compression.NativeUI.Views; /// again exactly as it was; nothing about it is rebuilt. /// internal sealed partial class MainForm { - private const int FieldWidth = 160; + private const int FieldWidth = 188; private const int CaptionWidth = 72; private readonly RibbonContextualTabGroup _diskTools = new("Disk Tools", Color.FromArgb(0x3A, 0x8E, 0x5C)) { Visible = false }; From 0dadfa6d0024958ec2c10f831bc5e8e11f81133c Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 08:43:38 +0200 Subject: [PATCH 05/16] # the Options fields no longer cut their text off on GTK Symptom: on GTK the Metadata and Profile fields showed only the top of "Unchanged" and "(none)". Root cause: GTK draws a native combo box at its natural height whatever bounds it is given, and that is taller than a ribbon row. Fix: the fields are an owner-drawn ChoiceBox (a DropDownButton whose menu holds the choices), which keeps exactly the bounds the ribbon lays out. A test pins every hosted input inside its row and rules out native combo boxes there. --- Compression.NativeUI/Controls/ChoiceBox.cs | 54 +++++++++++++++++++ .../Views/MainForm.Defragment.cs | 21 ++++---- .../NativeUI/MainFormTests.Defragment.cs | 31 +++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 Compression.NativeUI/Controls/ChoiceBox.cs diff --git a/Compression.NativeUI/Controls/ChoiceBox.cs b/Compression.NativeUI/Controls/ChoiceBox.cs new file mode 100644 index 000000000..e8ee8ba0b --- /dev/null +++ b/Compression.NativeUI/Controls/ChoiceBox.cs @@ -0,0 +1,54 @@ +using Hawkynt.NativeForms; + +namespace Compression.NativeUI.Controls; + +/// +/// A drop-down list drawn by the toolkit rather than the platform: the face shows the picked item +/// and a click opens the choices as a menu. +/// +/// +/// It stands in for where the space is fixed and small — a ribbon row. GTK +/// draws a native combo box at its natural height whatever bounds it is given, so in a ribbon row +/// the bottom of its text is cut off; an owner-drawn face keeps exactly the bounds it is laid out to. +/// +internal sealed class ChoiceBox : DropDownButton { + private readonly List _items = []; + private int _selectedIndex = -1; + + /// Raised after changes, whether by a click or by code. + public event EventHandler? SelectedIndexChanged; + + public IReadOnlyList Items => this._items; + + /// The picked item's index, or -1 for none. Out-of-range values are clamped. + public int SelectedIndex { + get => this._selectedIndex; + set { + var index = this._items.Count == 0 ? -1 : Math.Clamp(value, 0, this._items.Count - 1); + if (index == this._selectedIndex) return; + + this._selectedIndex = index; + this.Text = index < 0 ? "" : this._items[index]; + for (var i = 0; i < this.DropDownItems.Count; ++i) + if (this.DropDownItems[i] is ToolStripMenuItem item) item.Checked = i == index; + this.SelectedIndexChanged?.Invoke(this, EventArgs.Empty); + } + } + + /// Replaces the choices and picks . + public void SetItems(IEnumerable items, int selectedIndex = 0) { + this._items.Clear(); + this._items.AddRange(items); + this.DropDownItems.Clear(); + for (var i = 0; i < this._items.Count; ++i) { + var index = i; + var item = new ToolStripMenuItem(this._items[i]); + item.Click += (_, _) => this.SelectedIndex = index; + this.DropDownItems.Add(item); + } + + // Forced through the setter even when the index is unchanged, so the face shows the new text. + this._selectedIndex = -2; + this.SelectedIndex = selectedIndex; + } +} diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index 8276a0ed7..0c71ed934 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -46,12 +46,12 @@ internal sealed partial class MainForm { private RibbonButton _editProfilesButton = null!; private readonly TextBox _interleaveBox = new() { Text = "1" }; - private readonly ComboBox _metadataZoneCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; - private readonly ComboBox _layoutProfileCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly ChoiceBox _metadataZoneCombo = new(); + private readonly ChoiceBox _layoutProfileCombo = new(); private readonly TextBox _holeSizeBox = new() { Text = "64m" }; private readonly TextBox _holeAtBox = new() { Text = "auto" }; private readonly TextBox _seedBox = new() { Text = "1" }; - private readonly ComboBox _placementCombo = new() { DropDownStyle = ComboBoxStyle.DropDownList }; + private readonly ChoiceBox _placementCombo = new(); private RibbonHostItem _interleaveItem = null!; private RibbonHostItem _metadataZoneItem = null!; private RibbonHostItem _layoutProfileItem = null!; @@ -154,8 +154,7 @@ private RibbonGroup StrategyGroup() { /// private RibbonGroup OptionsGroup() { // Short, because the field is narrow; the tooltip says what each placement means. - this._metadataZoneCombo.Items.AddRange(["Unchanged", "Front", "Back", "Middle", "Before data"]); - this._metadataZoneCombo.SelectedIndex = 0; + this._metadataZoneCombo.SetItems(["Unchanged", "Front", "Back", "Middle", "Before data"]); this.RefreshLayoutProfiles(); this._interleaveItem = this.Field("Interleave", this._interleaveBox, @@ -171,8 +170,7 @@ private RibbonGroup OptionsGroup() { this.RefreshLayoutProfiles(); }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); - this._placementCombo.Items.AddRange(["Default", "Metadata first", "Data first"]); - this._placementCombo.SelectedIndex = 0; + this._placementCombo.SetItems(["Default", "Metadata first", "Data first"]); this._seedItem = this.Field("Seed", this._seedBox, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", () => this.WithPresenter(p => p.SeedText = this._seedBox.Text)); @@ -223,7 +221,7 @@ private RibbonHostItem Field(string caption, Control input, string tip, Action c switch (input) { case TextBox box: box.TextChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; - case ComboBox combo: combo.SelectedIndexChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; + case ChoiceBox combo: combo.SelectedIndexChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; } this._ribbonTips.SetToolTip(input, tip); @@ -350,13 +348,12 @@ private void RefreshLayoutProfiles() { this._syncingDefragRibbon = true; try { - this._layoutProfileCombo.Items.Clear(); this._layoutProfileEntries.Clear(); - this._layoutProfileCombo.Items.Add("(none)"); + var names = new List { "(none)" }; this._layoutProfileEntries.Add(null); try { foreach (var entry in LayoutProfileStore.List()) { - this._layoutProfileCombo.Items.Add($"{entry.Name} [{(entry.Origin == ProfileOrigin.Builtin ? "Built-in" : "User")}]"); + names.Add($"{entry.Name} [{(entry.Origin == ProfileOrigin.Builtin ? "Built-in" : "User")}]"); this._layoutProfileEntries.Add(entry); } } catch { @@ -364,7 +361,7 @@ private void RefreshLayoutProfiles() { } var keep = this._layoutProfileEntries.FindIndex(e => e is not null && string.Equals(e.FilePath, previous, StringComparison.OrdinalIgnoreCase)); - this._layoutProfileCombo.SelectedIndex = Math.Max(0, keep); + this._layoutProfileCombo.SetItems(names, Math.Max(0, keep)); } finally { this._syncingDefragRibbon = false; } diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs index 5f2fe7d53..767ca440e 100644 --- a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -260,6 +260,37 @@ public void GivenAContextMenuVerb_WhenRun_ThenTheDefragmentTabOpensWithThatOpera })); } + /// + /// A hosted field gets one stacked row. GTK draws a native combo box at its natural height + /// whatever bounds it is handed, which cut the bottom off its text, so every hosted input must + /// stay within its row and none may be a native combo box. + /// + [Test] + public void GivenTheOptionsFields_WhenTheTabIsLaidOut_ThenEveryHostedInputFitsItsRow() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + Toggle(shell, "Carve Hole").PerformClick(); + var ribbon = Field(shell, "_ribbon"); + ribbon.PerformLayout(); + + var hosted = DefragTab(shell).Groups + .SelectMany(g => g.Items.Cast().OfType().Where(h => h.Visible).Select(h => (Group: g, Host: h))) + .ToList(); + Assert.That(hosted, Is.Not.Empty); + + Assert.Multiple(() => { + foreach (var (group, host) in hosted) { + var row = group.Bounds.Height / 3; + Assert.That(host.Control.Height, Is.LessThanOrEqualTo(row), $"{host.Text} is taller than a row"); + foreach (Control input in host.Control.Controls) { + Assert.That(input.Height, Is.LessThanOrEqualTo(host.Control.Height), $"{host.Text}: {input.GetType().Name}"); + Assert.That(input, Is.Not.InstanceOf(), $"{host.Text}: a native combo box ignores its row height on GTK"); + } + } + }); + })); + } + // ── running ───────────────────────────────────────────────────────────────────────────────── [Test] From 69421cb3fa249dfd6b5a0195edebf0188bdd0f68 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 08:43:39 +0200 Subject: [PATCH 06/16] # the run log wraps long lines instead of cutting them off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: "SUCCEEDED (31 ms) — Defragmentation complete — 4…" ran off the right edge of the log under the files panel. Root cause: a multiline TextBox neither wraps nor scrolls sideways on every backend. Fix: the log is drawn by LogView, which wraps each line to its width (at spaces, inside a word only when the word alone is too wide), keeps the newest line in view and scrolls back with the mouse wheel. --- Compression.NativeUI/Controls/LogView.cs | 95 ++++++++++++++++++++ Compression.NativeUI/Views/DefragmentView.cs | 9 +- 2 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 Compression.NativeUI/Controls/LogView.cs diff --git a/Compression.NativeUI/Controls/LogView.cs b/Compression.NativeUI/Controls/LogView.cs new file mode 100644 index 000000000..276b3ceb6 --- /dev/null +++ b/Compression.NativeUI/Controls/LogView.cs @@ -0,0 +1,95 @@ +using System.Drawing; +using Hawkynt.NativeForms; +using Hawkynt.NativeForms.Drawing; + +namespace Compression.NativeUI.Controls; + +/// +/// A read-only log that wraps every line to its width and keeps the newest line in view; the mouse +/// wheel scrolls back through older ones. +/// +/// +/// A multiline text box neither wraps nor scrolls sideways on every backend, so a long outcome line +/// ran off its right edge. Drawing the lines here wraps them the same way everywhere. +/// +internal sealed class LogView : OwnerDrawnControl { + private const int Inset = 3; + + private readonly List _lines = []; + private int _scrollBack; + + /// Every line logged since the last , unwrapped. + public string LogText => string.Join(Environment.NewLine, this._lines); + + public void Append(string line) { + this._lines.AddRange(line.Replace("\r\n", "\n").Split('\n')); + this._scrollBack = 0; + this.Invalidate(); + } + + public void Clear() { + this._lines.Clear(); + this._scrollBack = 0; + this.Invalidate(); + } + + /// + /// Breaks into rows no wider than : at spaces where + /// it can, inside a word only when the word alone is wider than the row. + /// + internal static List Wrap(string line, int width, Func measure) { + var rows = new List(); + if (line.Length == 0 || width <= 0 || measure(line) <= width) { + rows.Add(line); + return rows; + } + + var rest = line; + while (rest.Length > 0) { + if (measure(rest) <= width) { + rows.Add(rest); + break; + } + + // The longest prefix that fits, found by halving rather than one character at a time. + int lo = 1, hi = rest.Length; + while (lo < hi) { + var mid = (lo + hi + 1) / 2; + if (measure(rest[..mid]) <= width) lo = mid; + else hi = mid - 1; + } + + var cut = lo; + var space = rest.LastIndexOf(' ', Math.Min(cut, rest.Length - 1)); + if (space > 0) cut = space; + rows.Add(rest[..cut].TrimEnd()); + rest = rest[cut..].TrimStart(); + } + + return rows; + } + + protected override void OnPaint(PaintEventArgs e) { + var g = e.Graphics; + var theme = DefaultTheme.Instance; + g.FillRectangle(theme.FieldBackground, new(0, 0, this.Width, this.Height)); + g.DrawRectangle(theme.Border, new(0, 0, this.Width - 1, this.Height - 1)); + + var width = this.Width - 2 * Inset; + var rowHeight = Math.Max(1, g.MeasureText("Ag", this.Font).Height); + var rows = this._lines.SelectMany(l => Wrap(l, width, s => g.MeasureText(s, this.Font).Width)).ToList(); + var visible = Math.Max(1, (this.Height - 2 * Inset) / rowHeight); + this._scrollBack = Math.Clamp(this._scrollBack, 0, Math.Max(0, rows.Count - visible)); + + var first = Math.Max(0, rows.Count - visible - this._scrollBack); + var y = Inset; + for (var i = first; i < rows.Count && y + rowHeight <= this.Height - Inset + 1; ++i, y += rowHeight) + g.DrawText(rows[i], this.Font, theme.ControlText, new(Inset, y, width, rowHeight), ContentAlignment.MiddleLeft); + } + + protected override void OnMouseWheel(MouseEventArgs e) { + this._scrollBack += e.Delta > 0 ? 3 : -3; + if (this._scrollBack < 0) this._scrollBack = 0; + this.Invalidate(); + } +} diff --git a/Compression.NativeUI/Views/DefragmentView.cs b/Compression.NativeUI/Views/DefragmentView.cs index 066b7766c..7cc10f8d7 100644 --- a/Compression.NativeUI/Views/DefragmentView.cs +++ b/Compression.NativeUI/Views/DefragmentView.cs @@ -25,7 +25,7 @@ internal sealed class DefragmentView : Panel { private readonly SplitContainer _split = new() { FixedPanel = FixedPanel.Panel2, Panel1MinSize = 160, Panel2MinSize = 200, Dock = DockStyle.Fill }; private readonly Label _filesStatus = new() { Text = "—", ForeColor = Color.DimGray, Dock = DockStyle.Top, Height = 18 }; private readonly DataGridView _files = new() { ReadOnly = true, ShowGridLines = true, Dock = DockStyle.Fill }; - private readonly TextBox _log = new() { ReadOnly = true, Multiline = true, Font = MonoFont, Dock = DockStyle.Bottom, Height = LogHeight }; + private readonly LogView _log = new() { Font = MonoFont, Dock = DockStyle.Bottom, Height = LogHeight }; private readonly LegendStrip _legend = new() { Dock = DockStyle.Bottom, Height = LegendHeight }; private readonly ProgressBar _progress = new() { Minimum = 0, Maximum = 100, Dock = DockStyle.Bottom, Height = ProgressHeight }; private TileContentsWindow? _tileWindow; @@ -91,7 +91,7 @@ public bool LegendVisible { private bool _legendVisible = true; /// Every line logged since the view was last reset. - public string LogText => this._log.Text; + public string LogText => this._log.LogText; public int ProgressValue { get => this._progress.Value; @@ -129,14 +129,13 @@ public void Apply(MaintenanceProgress progress) { } public void Log(string line) { - this._log.AppendText(line + Environment.NewLine); - this._log.SelectionStart = this._log.Text.Length; + this._log.Append(line); } /// Forgets the previous image: no map, no rows, no log. public void Clear() { this.ShowSnapshot(BlockMapSnapshot.Empty("—")); - this._log.Text = ""; + this._log.Clear(); this.ProgressValue = 0; } From 44e3cb6da40b713e8ce52b3ed5cea5cdd394093b Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 08:43:39 +0200 Subject: [PATCH 07/16] # the files panel shows every file's fragment count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: on a FAT16 volume written by Linux the Frag column read "—" for every file while the status line said one file was fragmented. Root cause: the counts were keyed by the layout walker's names and looked up by the lister's. FatExtentMap reports a short entry's stored 8.3 name (BIG.BIN) and ignores the lowercase flags that the lister applies (big.bin), so no name matched. The runs were also counted after the extents had been sorted by offset, which merges a file stored backwards into one run. Fix: an exact match is tried first, then a match ignoring case and a leading slash, used only when it is unambiguous. Runs are counted in the order the walker yields, which is the file's own order. Tests cover upper- and lowercase FAT fixtures against the extent map's own counts, the lookup rules, and log wrapping. --- .../Maintenance/BlockMapSnapshot.cs | 63 ++++++++++++++++--- .../NativeUI/MaintenanceFixtures.cs | 11 +++- .../NativeUI/MaintenancePresenterTests.cs | 58 +++++++++++++++++ 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs b/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs index 0a7cefd0f..9089c1367 100644 --- a/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs +++ b/Compression.NativeUI/Maintenance/BlockMapSnapshot.cs @@ -56,15 +56,19 @@ public static BlockMapSnapshot Read(string path, IArchiveFormatOperations? ops) var imageSize = stream.Length; if (imageSize <= 0) return null; - var filled = NormalizeExtents([.. extentMap.EnumerateExtents(stream)], imageSize); + // Runs are counted in the order the walker yields — the file's own order — before the extents + // are sorted by offset for drawing: sorted, a file stored backwards would look contiguous. + List raw = [.. extentMap.EnumerateExtents(stream)]; + var runs = OwnerLookup.From(CountRunsByOwner(raw).Select(r => (r.Key, r.Value))); + var filled = NormalizeExtents(raw, imageSize); if (filled is null) return null; - Dictionary? mtimeByName = null; + OwnerLookup? mtimeByName = null; List? entries = null; try { stream.Position = 0; entries = ops.List(stream, password: null); - mtimeByName = entries.Where(e => !e.IsDirectory).GroupBy(e => e.Name).ToDictionary(g => g.Key, g => g.First().LastModified); + mtimeByName = OwnerLookup.From(entries.Where(e => !e.IsDirectory).Select(e => (e.Name, e.LastModified))); } catch { // Classification is best-effort; the layout itself is what matters here. } @@ -76,7 +80,6 @@ public static BlockMapSnapshot Read(string path, IArchiveFormatOperations? ops) ? ex with { Classification = thresholds is null ? DefragBlockClass.Normal : Classify(mtime, thresholds, 0, 1) } : ex); - var runs = CountRunsByOwner(classified); IReadOnlyList rows = entries is null ? [] : [.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { Name = e.Name, Size = e.OriginalSize, @@ -100,15 +103,17 @@ public static BlockMapSnapshot Read(string path, IArchiveFormatOperations? ops) var imageSize = stream.Length; if (imageSize <= 0) return null; - var filled = NormalizeExtents([.. archiveLayout.EnumerateLayout(stream)], imageSize); + List raw = [.. archiveLayout.EnumerateLayout(stream)]; + var runs = OwnerLookup.From(CountRunsByOwner(raw).Select(r => (r.Key, r.Value))); + var filled = NormalizeExtents(raw, imageSize); if (filled is null) return null; - Dictionary? methodByName = null; + OwnerLookup? methodByName = null; List? entries = null; try { stream.Position = 0; entries = ops.List(stream, password: null); - methodByName = entries.Where(e => !e.IsDirectory).GroupBy(e => e.Name).ToDictionary(g => g.Key, g => g.First().Method ?? ""); + methodByName = OwnerLookup.From(entries.Where(e => !e.IsDirectory).Select(e => (e.Name, e.Method ?? ""))); } catch { // Classification is best-effort. } @@ -119,9 +124,7 @@ public static BlockMapSnapshot Read(string path, IArchiveFormatOperations? ops) ? ex with { Classification = ClassifyByMethod(method) } : ex); - // Counted off the layout the container reported. An entry the map does not name is not one run - // — it is unmeasured. - var runs = CountRunsByOwner(classified); + // An entry the map does not name is not one run — it is unmeasured. IReadOnlyList rows = entries is null ? [] : [.. entries.Where(e => !e.IsDirectory).Select(e => new FileRow { Name = e.Name, Size = e.OriginalSize, @@ -387,3 +390,43 @@ internal static Dictionary CountRunsByOwner(IEnumerable +/// Finds what a layout map says about a file by the name the listing gives it. A format's layout +/// walker and its lister do not always spell a name alike — FAT's walker keeps the stored 8.3 name +/// in capitals while the lister applies the lowercase flags, so BIG.BIN in the map is +/// big.bin in the list — so a name that does not match exactly is matched ignoring case and +/// separators, but only when that match is unambiguous. +/// +internal sealed class OwnerLookup { + private readonly Dictionary _exact = new(StringComparer.Ordinal); + private readonly Dictionary _folded = new(StringComparer.OrdinalIgnoreCase); + + internal OwnerLookup(IEnumerable<(string Name, T Value)> pairs) { + foreach (var (name, value) in pairs) { + var key = Normalize(name); + if (!this._exact.TryAdd(key, value)) continue; + this._folded[key] = this._folded.TryGetValue(key, out var seen) ? (seen.Value, true) : (value, false); + } + } + + public IEnumerable Values => this._exact.Values; + + public bool TryGetValue(string name, out T value) { + var key = Normalize(name); + if (this._exact.TryGetValue(key, out value!)) return true; + if (this._folded.TryGetValue(key, out var folded) && !folded.Ambiguous) { + value = folded.Value; + return true; + } + + value = default!; + return false; + } + + private static string Normalize(string name) => name.Replace('\\', '/').TrimStart('/'); +} + +internal static class OwnerLookup { + public static OwnerLookup From(IEnumerable<(string Name, T Value)> pairs) => new(pairs); +} diff --git a/Compression.Tests/NativeUI/MaintenanceFixtures.cs b/Compression.Tests/NativeUI/MaintenanceFixtures.cs index 348a12a4d..bb9b4739f 100644 --- a/Compression.Tests/NativeUI/MaintenanceFixtures.cs +++ b/Compression.Tests/NativeUI/MaintenanceFixtures.cs @@ -20,10 +20,17 @@ internal static class MaintenanceFixtures { ["ECHO.LOG"] = Pattern(3_300, 0x55), }; + /// + /// The same contents under lowercase 8.3 names, as Linux writes them: a short entry with the + /// lowercase flags set and no long name, which FAT's layout walker reports in capitals. + /// + public static readonly IReadOnlyDictionary LowercaseFatFiles + = FatFiles.ToDictionary(f => f.Key.ToLowerInvariant(), f => f.Value); + /// A 1.44 MB FAT12 floppy whose files are scattered on purpose, so defragmenting moves something. - public static string FragmentedFat(string folder, string name = "volume.img") { + public static string FragmentedFat(string folder, string name = "volume.img", IReadOnlyDictionary? files = null) { var writer = new FileSystem.Fat.FatWriter(); - foreach (var (file, data) in FatFiles) writer.AddFile(file, data); + foreach (var (file, data) in files ?? FatFiles) writer.AddFile(file, data); var path = Path.Combine(folder, name); File.WriteAllBytes(path, writer.Build()); diff --git a/Compression.Tests/NativeUI/MaintenancePresenterTests.cs b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs index cb20b342f..c31694a2e 100644 --- a/Compression.Tests/NativeUI/MaintenancePresenterTests.cs +++ b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs @@ -179,6 +179,64 @@ public void GivenAnInvalidInterleave_WhenAnotherOperationIsPicked_ThenItDoesNotB Assert.That(presenter.StartBlocker, Is.Null, "interleave only applies to Defragment"); } + // ── the files panel ───────────────────────────────────────────────────────────────────────── + + /// + /// Linux writes a lowercase 8.3 name as a short entry with the lowercase flags; FAT's layout walker + /// reports it in capitals while the lister reports it in lowercase. The fragment counts were looked + /// up by the listed name and so came out as an em dash for every file. + /// + [TestCase(false)] + [TestCase(true)] + public void GivenAFragmentedFat_WhenAnalyzed_ThenEveryFileShowsTheRunsTheLayoutMapGivesIt(bool lowercase) { + var files = lowercase ? MaintenanceFixtures.LowercaseFatFiles : MaintenanceFixtures.FatFiles; + var path = MaintenanceFixtures.FragmentedFat(this._root, files: files); + System.Collections.Generic.Dictionary expected; + using (var stream = File.OpenRead(path)) + expected = BlockMapSnapshot.CountRunsByOwner([.. ((IFilesystemExtentMap)FormatRegistry.GetArchiveOps("Fat")!).EnumerateExtents(stream)]); + + var snapshot = new MaintenancePresenter(path, "Fat").Analyze(); + + Assert.Multiple(() => { + Assert.That(snapshot.Rows.Select(r => r.Name), Is.EquivalentTo(files.Keys)); + foreach (var row in snapshot.Rows) { + var runs = expected.Single(e => string.Equals(e.Key, row.Name, StringComparison.OrdinalIgnoreCase)).Value; + Assert.That(row.FragmentsDisplay, Is.EqualTo(runs.ToString("N0")), row.Name); + } + Assert.That(snapshot.Rows.Count(r => int.Parse(r.FragmentsDisplay) > 1), Is.GreaterThan(0), "the fixture is fragmented"); + }); + } + + [Test] + public void GivenLayoutOwners_WhenLookedUpByListedName_ThenExactWinsAndCaseOrSlashDifferencesMatchOnlyWhenUnambiguous() { + var lookup = OwnerLookup.From([("BIG.BIN", 3), ("/docs/Note.txt", 2), ("a.txt", 1), ("A.TXT", 5)]); + + Assert.Multiple(() => { + Assert.That(lookup.TryGetValue("big.bin", out var big) ? big : -1, Is.EqualTo(3), "case differs only"); + Assert.That(lookup.TryGetValue("docs/note.txt", out var note) ? note : -1, Is.EqualTo(2), "leading slash and case"); + Assert.That(lookup.TryGetValue("A.TXT", out var exact) ? exact : -1, Is.EqualTo(5), "an exact match wins"); + Assert.That(lookup.TryGetValue("A.txt", out _), Is.False, "two owners differ only by case: no guess"); + Assert.That(lookup.TryGetValue("missing.bin", out _), Is.False); + }); + } + + // ── the run log ───────────────────────────────────────────────────────────────────────────── + + [TestCase("SUCCEEDED (31 ms) — Defragmentation complete — 46 moves, 0 bytes staged in memory", 20)] + [TestCase("short", 20)] + [TestCase("averyveryverylongwordwithoutanyspacesatallthatmustbebroken", 10)] + [TestCase("", 10)] + public void GivenALogLine_WhenWrappedToAWidth_ThenNoRowIsWiderAndNothingIsLost(string line, int columns) { + static int Measure(string s) => s.Length; // one unit per character + + var rows = global::Compression.NativeUI.Controls.LogView.Wrap(line, columns, Measure); + + Assert.Multiple(() => { + Assert.That(rows.All(r => Measure(r) <= columns), Is.True, string.Join(" | ", rows)); + Assert.That(string.Concat(rows).Replace(" ", ""), Is.EqualTo(line.Replace(" ", "")), "only spaces at a break may go"); + }); + } + // ── running ───────────────────────────────────────────────────────────────────────────────── [Test] From d9aafe59ee21d03736e00d639c98e1c70870b3a5 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 09:23:06 +0200 Subject: [PATCH 08/16] * the shell builds against NativeForms 1e14385, which adds ribbon combo boxes and spinners and keeps the window frame outside the client area --- build/NativeForms.ref | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/NativeForms.ref b/build/NativeForms.ref index a7f5ea30d..2c5b0b52e 100644 --- a/build/NativeForms.ref +++ b/build/NativeForms.ref @@ -1 +1 @@ -69bf60fa8cbc2c1f781082c41729f316085ca1fe +1e1438506f6240c3eb7f3d616cce9ea374b84d61 From 99750efee5dd97607a1f32711f9aa5ec62ecde1a Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Thu, 1 Oct 2026 09:27:21 +0200 Subject: [PATCH 09/16] * the Defragment tab's fields are ribbon combo boxes and spinners NativeForms 1e14385 draws RibbonComboBox and RibbonSpinner itself: always one stacked row, and still usable from a collapsed group's popup, which a hosted control cannot follow into. They replace the hosted text boxes and the owner-drawn ChoiceBox. - Interleave is a spinner over 1-256, so an out-of-range stride can no longer be typed; it clamps. - Metadata placement, layout profile and chunk placement are combo boxes. - Carve Hole takes a size in KiB, a placement (End / Offset) and an offset in KiB, which is enabled only for Offset. - Seed is a spinner. Tests: every input on the tab is a one-row ribbon field and none is a hosted control; the interleave spinner clamps 0 and 257; the carve fields reach the engine as bytes. --- Compression.NativeUI/Controls/ChoiceBox.cs | 54 ------ .../Views/MainForm.Defragment.cs | 171 ++++++++---------- .../NativeUI/MainFormTests.Defragment.cs | 91 ++++++---- 3 files changed, 132 insertions(+), 184 deletions(-) delete mode 100644 Compression.NativeUI/Controls/ChoiceBox.cs diff --git a/Compression.NativeUI/Controls/ChoiceBox.cs b/Compression.NativeUI/Controls/ChoiceBox.cs deleted file mode 100644 index e8ee8ba0b..000000000 --- a/Compression.NativeUI/Controls/ChoiceBox.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Hawkynt.NativeForms; - -namespace Compression.NativeUI.Controls; - -/// -/// A drop-down list drawn by the toolkit rather than the platform: the face shows the picked item -/// and a click opens the choices as a menu. -/// -/// -/// It stands in for where the space is fixed and small — a ribbon row. GTK -/// draws a native combo box at its natural height whatever bounds it is given, so in a ribbon row -/// the bottom of its text is cut off; an owner-drawn face keeps exactly the bounds it is laid out to. -/// -internal sealed class ChoiceBox : DropDownButton { - private readonly List _items = []; - private int _selectedIndex = -1; - - /// Raised after changes, whether by a click or by code. - public event EventHandler? SelectedIndexChanged; - - public IReadOnlyList Items => this._items; - - /// The picked item's index, or -1 for none. Out-of-range values are clamped. - public int SelectedIndex { - get => this._selectedIndex; - set { - var index = this._items.Count == 0 ? -1 : Math.Clamp(value, 0, this._items.Count - 1); - if (index == this._selectedIndex) return; - - this._selectedIndex = index; - this.Text = index < 0 ? "" : this._items[index]; - for (var i = 0; i < this.DropDownItems.Count; ++i) - if (this.DropDownItems[i] is ToolStripMenuItem item) item.Checked = i == index; - this.SelectedIndexChanged?.Invoke(this, EventArgs.Empty); - } - } - - /// Replaces the choices and picks . - public void SetItems(IEnumerable items, int selectedIndex = 0) { - this._items.Clear(); - this._items.AddRange(items); - this.DropDownItems.Clear(); - for (var i = 0; i < this._items.Count; ++i) { - var index = i; - var item = new ToolStripMenuItem(this._items[i]); - item.Click += (_, _) => this.SelectedIndex = index; - this.DropDownItems.Add(item); - } - - // Forced through the setter even when the index is unchanged, so the face shows the new text. - this._selectedIndex = -2; - this.SelectedIndex = selectedIndex; - } -} diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index 0c71ed934..e46f07157 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -7,7 +7,6 @@ using Compression.Registry; using Compression.Registry.Layout; using Hawkynt.NativeForms; -using Hawkynt.NativeForms.Drawing; using Color = System.Drawing.Color; namespace Compression.NativeUI.Views; @@ -25,12 +24,9 @@ namespace Compression.NativeUI.Views; /// again exactly as it was; nothing about it is rebuilt. /// internal sealed partial class MainForm { - private const int FieldWidth = 188; - private const int CaptionWidth = 72; private readonly RibbonContextualTabGroup _diskTools = new("Disk Tools", Color.FromArgb(0x3A, 0x8E, 0x5C)) { Visible = false }; private readonly DefragmentView _defragView = new() { Visible = false }; - private readonly ToolTip _ribbonTips = new(); private RibbonTab _defragTab = null!; private readonly Dictionary _verbToggles = []; @@ -45,20 +41,18 @@ internal sealed partial class MainForm { private RibbonButton _analyzeButton = null!; private RibbonButton _editProfilesButton = null!; - private readonly TextBox _interleaveBox = new() { Text = "1" }; - private readonly ChoiceBox _metadataZoneCombo = new(); - private readonly ChoiceBox _layoutProfileCombo = new(); - private readonly TextBox _holeSizeBox = new() { Text = "64m" }; - private readonly TextBox _holeAtBox = new() { Text = "auto" }; - private readonly TextBox _seedBox = new() { Text = "1" }; - private readonly ChoiceBox _placementCombo = new(); - private RibbonHostItem _interleaveItem = null!; - private RibbonHostItem _metadataZoneItem = null!; - private RibbonHostItem _layoutProfileItem = null!; - private RibbonHostItem _holeSizeItem = null!; - private RibbonHostItem _holeAtItem = null!; - private RibbonHostItem _seedItem = null!; - private RibbonHostItem _placementItem = null!; + // Ribbon fields, drawn by the ribbon: one row tall on every backend, and still usable from a + // collapsed group's popup. + private readonly RibbonSpinner _interleaveSpinner = new("Interleave") { + Minimum = MaintenanceInput.MinInterleave, Maximum = MaintenanceInput.MaxInterleave, Value = 1, FieldWidth = 56, + }; + private readonly RibbonComboBox _metadataZoneCombo = new("Metadata") { FieldWidth = 112 }; + private readonly RibbonComboBox _layoutProfileCombo = new("Profile") { FieldWidth = 112 }; + private readonly RibbonSpinner _holeSizeSpinner = new("Hole KiB") { Minimum = 1, Maximum = 1L << 32, Value = 64, Increment = 64, FieldWidth = 80 }; + private readonly RibbonComboBox _holePlacementCombo = new("Hole at") { FieldWidth = 80 }; + private readonly RibbonSpinner _holeOffsetSpinner = new("Offset KiB") { Minimum = 0, Maximum = 1L << 32, Value = 0, Increment = 64, FieldWidth = 80 }; + private readonly RibbonSpinner _seedSpinner = new("Seed") { Minimum = int.MinValue, Maximum = int.MaxValue, Value = 1, FieldWidth = 80 }; + private readonly RibbonComboBox _placementCombo = new("Chunks") { FieldWidth = 112 }; private readonly List _layoutProfileEntries = []; private MaintenanceSession? _maintenanceSession; @@ -139,42 +133,45 @@ private RibbonGroup StrategyGroup() { @checked: false, on => this.WithPresenter(p => p.PackAtEnd = on)); group.Items.Add(this._packAtEndToggle); - this._holeSizeItem = this.Field("Hole size", this._holeSizeBox, "Size in bytes. Suffixes: k=KiB, m=MiB, g=GiB. Examples: 64m, 1g, 524288.", - () => this.WithPresenter(p => p.HoleSizeText = this._holeSizeBox.Text)); - this._holeAtItem = this.Field("Hole at", this._holeAtBox, "Byte offset where the hole starts. 'auto' = after the last live extent.", - () => this.WithPresenter(p => p.HoleAtText = this._holeAtBox.Text)); - group.Items.AddRange(this._holeSizeItem, this._holeAtItem); + this._holePlacementCombo.Items.AddRange(["End", "Offset"]); + this._holePlacementCombo.SelectedIndex = 0; + this.Field(this._holeSizeSpinner, "Size of the hole to carve, in KiB.", () => this.WithPresenter(p => p.HoleSizeText = HoleSizeText())); + this.Field(this._holePlacementCombo, "End: after the last live extent. Offset: at the offset beside.", + () => this.WithPresenter(p => p.HoleAtText = HoleAtText())); + this.Field(this._holeOffsetSpinner, "Where the hole starts, in KiB from the start of the image.", + () => this.WithPresenter(p => p.HoleAtText = HoleAtText())); + group.Items.AddRange(this._holeSizeSpinner, this._holePlacementCombo, this._holeOffsetSpinner); return group; } /// /// The layout options of a defragmentation, then — below Edit Profiles, shown only while their /// operation is picked — the one option Scramble, Optimize or Compact takes. Showing only the one - /// that applies keeps the tab narrow enough that no group with a hosted field has to fold away. + /// that applies keeps the tab narrow. /// private RibbonGroup OptionsGroup() { // Short, because the field is narrow; the tooltip says what each placement means. - this._metadataZoneCombo.SetItems(["Unchanged", "Front", "Back", "Middle", "Before data"]); + this._metadataZoneCombo.Items.AddRange(["Unchanged", "Front", "Back", "Middle", "Before data"]); + this._metadataZoneCombo.SelectedIndex = 0; this.RefreshLayoutProfiles(); - this._interleaveItem = this.Field("Interleave", this._interleaveBox, + this.Field(this._interleaveSpinner, $"Block interleave: 1 = contiguous, 2 = every other block, N = each file's Kth block at start + K×N. {MaintenanceInput.MinInterleave}–{MaintenanceInput.MaxInterleave}.", - () => this.WithPresenter(p => p.InterleaveText = this._interleaveBox.Text)); - this._metadataZoneItem = this.Field("Metadata", this._metadataZoneCombo, "Where filesystem metadata and directory entries are placed: unchanged, at the front (fast access), at the back (data first), in the middle (least seeking) or just before the file content (read-ahead).", + () => this.WithPresenter(p => p.InterleaveText = InterleaveText())); + this.Field(this._metadataZoneCombo, "Where filesystem metadata and directory entries are placed: unchanged, at the front (fast access), at the back (data first), in the middle (least seeking) or just before the file content (read-ahead).", () => this.WithPresenter(p => p.MetadataZone = this.SelectedMetadataZone())); - this._layoutProfileItem = this.Field("Profile", this._layoutProfileCombo, - "A zone-based layout template: files go into named byte ranges with per-zone sort orders.", + this.Field(this._layoutProfileCombo, "A zone-based layout template: files go into named byte ranges with per-zone sort orders.", this.OnLayoutProfileChanged); this._editProfilesButton = this.RibbonAction("Edit Profiles", IconKeys.Properties, () => { new LayoutProfileEditor().ShowDialog(this); this.RefreshLayoutProfiles(); }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); - this._placementCombo.SetItems(["Default", "Metadata first", "Data first"]); - - this._seedItem = this.Field("Seed", this._seedBox, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", - () => this.WithPresenter(p => p.SeedText = this._seedBox.Text)); - this._placementItem = this.Field("Chunks", this._placementCombo, "Where an optimized file puts its metadata chunks relative to the payload.", + this._placementCombo.Items.AddRange(["Default", "Metadata first", "Data first"]); + this._placementCombo.SelectedIndex = 0; + this.Field(this._seedSpinner, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", + () => this.WithPresenter(p => p.SeedText = SeedText())); + this.Field(this._placementCombo, "Where an optimized file puts its metadata chunks relative to the payload.", () => this.WithPresenter(p => p.ChunkPlacement = this._placementCombo.SelectedIndex switch { 1 => MetadataPlacementProfile.MetadataFirst, 2 => MetadataPlacementProfile.DataFirst, @@ -184,8 +181,8 @@ private RibbonGroup OptionsGroup() { "Compact rebuilds at the smallest geometry the format allows", @checked: false, on => this.WithPresenter(p => p.MinimalGeometry = on)); var group = new RibbonGroup("Options"); - group.Items.AddRange(this._interleaveItem, this._metadataZoneItem, this._layoutProfileItem, - this._editProfilesButton, this._seedItem, this._placementItem, this._minimalGeometryToggle); + group.Items.AddRange(this._interleaveSpinner, this._metadataZoneCombo, this._layoutProfileCombo, + this._editProfilesButton, this._seedSpinner, this._placementCombo, this._minimalGeometryToggle); return group; } @@ -212,23 +209,30 @@ private RibbonGroup ViewGroup() { return group; } - /// A hosted input with its caption in front, one stacked row tall. - private RibbonHostItem Field(string caption, Control input, string tip, Action changed) { - var panel = new Panel(); - var label = new Label { Text = caption, Dock = DockStyle.Left, Width = CaptionWidth }; - input.Dock = DockStyle.Fill; - panel.Controls.AddRange(input, label); - - switch (input) { - case TextBox box: box.TextChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; - case ChoiceBox combo: combo.SelectedIndexChanged += (_, _) => { if (!this._syncingDefragRibbon) { changed(); this.SyncDefragmentRibbon(); } }; break; + /// Wires a ribbon field: its tooltip, kept in , and what a change does. + private void Field(RibbonFieldItem field, string tip, Action changed) { + field.Tag = tip; + field.ToolTipText = tip; + void OnChanged(object? sender, EventArgs e) { + if (this._syncingDefragRibbon) return; + changed(); + this.SyncDefragmentRibbon(); } - this._ribbonTips.SetToolTip(input, tip); - this._ribbonTips.SetToolTip(label, tip); - return new RibbonHostItem(panel) { HostWidth = FieldWidth, Text = caption, ToolTipText = tip, Tag = tip }; + switch (field) { + case RibbonSpinner spinner: spinner.ValueChanged += OnChanged; break; + case RibbonComboBox combo: combo.SelectedIndexChanged += OnChanged; break; + } } + // The presenter reads typed values; the fields can only hold valid ones, so these never fail to parse. + private string InterleaveText() => ((int)this._interleaveSpinner.Value).ToString(System.Globalization.CultureInfo.InvariantCulture); + private string HoleSizeText() => $"{(long)this._holeSizeSpinner.Value}k"; + private string HoleAtText() => this._holePlacementCombo.SelectedIndex == 1 + ? ((long)this._holeOffsetSpinner.Value * 1024).ToString(System.Globalization.CultureInfo.InvariantCulture) + : "auto"; + private string SeedText() => ((int)this._seedSpinner.Value).ToString(System.Globalization.CultureInfo.InvariantCulture); + // ── the tab and the client area ───────────────────────────────────────────────────────────── /// Shows the contextual tab while there is something to maintain, and follows the target. @@ -284,10 +288,10 @@ private void OpenSessionForTarget() { this._maintenanceSession = this._model.OpenMaintenanceSession(); if (this._maintenanceSession is { } session) { this._presenter = new MaintenancePresenter(session.ImagePath, session.FormatId) { - InterleaveText = this._interleaveBox.Text, - HoleSizeText = this._holeSizeBox.Text, - HoleAtText = this._holeAtBox.Text, - SeedText = this._seedBox.Text, + InterleaveText = this.InterleaveText(), + HoleSizeText = this.HoleSizeText(), + HoleAtText = this.HoleAtText(), + SeedText = this.SeedText(), MetadataZone = this.SelectedMetadataZone(), }; this.OnLayoutProfileChanged(); @@ -361,7 +365,9 @@ private void RefreshLayoutProfiles() { } var keep = this._layoutProfileEntries.FindIndex(e => e is not null && string.Equals(e.FilePath, previous, StringComparison.OrdinalIgnoreCase)); - this._layoutProfileCombo.SetItems(names, Math.Max(0, keep)); + this._layoutProfileCombo.Items.Clear(); + this._layoutProfileCombo.Items.AddRange(names); + this._layoutProfileCombo.SelectedIndex = Math.Max(0, keep); } finally { this._syncingDefragRibbon = false; } @@ -426,25 +432,24 @@ private void SyncDefragmentRibbon() { this._packAtEndToggle.ToolTipText = $"Pack at End: {(caps is null ? NoTarget : !consolidate ? "Applies to Consolidate." : caps.PackAtEnd.Reason)}"; var carve = defrag && p!.Strategy == DefragStrategy.CarveHole; - this._holeSizeItem.Visible = carve; - this._holeAtItem.Visible = carve; - this.Mark(this._holeSizeItem, idle, carve && MaintenanceInput.ParseSize(this._holeSizeBox.Text).Error is { } sizeError ? sizeError : null); - this.Mark(this._holeAtItem, idle, carve && MaintenanceInput.ParseHoleAt(this._holeAtBox.Text).Error is { } atError ? atError : null); - - this.Option(this._interleaveItem, idle && defrag, caps?.Interleave, defrag, - caps?.Interleave.Supported == true && defrag ? MaintenanceInput.ParseInterleave(this._interleaveBox.Text).Error : null); - this.Option(this._metadataZoneItem, idle && defrag, caps?.MetadataZone, defrag, null); - this.Option(this._layoutProfileItem, idle && defrag, caps?.LayoutProfile, defrag, null); + foreach (var field in new RibbonFieldItem[] { this._holeSizeSpinner, this._holePlacementCombo, this._holeOffsetSpinner }) { + field.Visible = carve; + field.Enabled = idle; + } + this._holeOffsetSpinner.Enabled = idle && this._holePlacementCombo.SelectedIndex == 1; + + Option(this._interleaveSpinner, idle && defrag, caps?.Interleave, defrag); + Option(this._metadataZoneCombo, idle && defrag, caps?.MetadataZone, defrag); + Option(this._layoutProfileCombo, idle && defrag, caps?.LayoutProfile, defrag); this._editProfilesButton.Enabled = idle && defrag && caps?.LayoutProfile.Supported == true; var scramble = p?.Verb == MaintenanceVerb.Scramble; - this._seedItem.Visible = scramble; - this._placementItem.Visible = p?.Verb == MaintenanceVerb.Optimize && caps?.ChunkPlacement.Supported == true; + this._seedSpinner.Visible = scramble; + this._placementCombo.Visible = p?.Verb == MaintenanceVerb.Optimize && caps?.ChunkPlacement.Supported == true; this._minimalGeometryToggle.Visible = p?.Verb == MaintenanceVerb.Compact && caps?.MinimalGeometry.Supported == true; - this.Option(this._seedItem, idle && scramble, scramble ? Capability.Yes("Seeds Scramble's shuffle.") : null, scramble, - scramble ? MaintenanceInput.ParseSeed(this._seedBox.Text).Error : null, appliesTo: "Scramble"); + Option(this._seedSpinner, idle && scramble, scramble ? Capability.Yes("Seeds Scramble's shuffle.") : null, scramble, appliesTo: "Scramble"); var optimize = p?.Verb == MaintenanceVerb.Optimize; - this.Option(this._placementItem, idle && optimize, caps?.ChunkPlacement, optimize, null, appliesTo: "Optimize"); + Option(this._placementCombo, idle && optimize, caps?.ChunkPlacement, optimize, appliesTo: "Optimize"); var compact = p?.Verb == MaintenanceVerb.Compact; this._minimalGeometryToggle.Enabled = idle && compact && caps?.MinimalGeometry.Supported == true; this._minimalGeometryToggle.Checked = p?.MinimalGeometry == true && compact; @@ -464,33 +469,13 @@ private void SyncDefragmentRibbon() { } } - /// Enables a hosted option and explains it: supported, not applicable, refused, or mistyped. - private void Option(RibbonHostItem item, bool applies, Capability? capability, bool relevant, string? error, string appliesTo = "Defragment") { - var supported = capability?.Supported == true; - item.Enabled = applies && supported; - item.Control.Enabled = item.Enabled; + /// Enables an option field and explains it: supported, not applicable, or refused. + private static void Option(RibbonFieldItem field, bool applies, Capability? capability, bool relevant, string appliesTo = "Defragment") { + field.Enabled = applies && capability?.Supported == true; var reason = capability is null ? "Open or select an image to maintain." : !relevant ? $"Applies to {appliesTo}." : capability.Value.Reason; - item.ToolTipText = error ?? $"{item.Text}: {reason} {item.Tag}"; - this.Flag(item, error); - } - - private void Mark(RibbonHostItem item, bool enabled, string? error) { - item.Enabled = enabled; - item.Control.Enabled = enabled; - item.ToolTipText = error ?? item.Tag as string; - this.Flag(item, error); - } - - /// A value that does not read turns its field pink, and the tooltip says what is wrong. - private void Flag(RibbonHostItem item, string? error) { - var input = item.Control.Controls[0]; - if (input is TextBox box) - box.BackColor = error is not null ? Color.FromArgb(0xFF, 0xD8, 0xD8) - : box.Enabled ? DefaultTheme.Instance.FieldBackground - : DefaultTheme.Instance.ControlBackground; - this._ribbonTips.SetToolTip(input, error ?? item.Tag as string); + field.ToolTipText = $"{field.Text}: {reason} {field.Tag}"; } // ── running ───────────────────────────────────────────────────────────────────────────────── diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs index 767ca440e..7e7790df9 100644 --- a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -159,7 +159,7 @@ public void GivenAZip_WhenOnTheTab_ThenDefragmentIsNotOfferedButOptimizeAndPurge public void GivenDefragment_WhenAnotherOperationIsPicked_ThenTheDefragModesAndLayoutOptionsGreyOut() { WithImages(root => WithShell(shell => { OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); - var interleave = Field(shell, "_interleaveItem"); + var interleave = Field(shell, "_interleaveSpinner"); Assert.That((Toggle(shell, "Consolidate").Enabled, interleave.Enabled), Is.EqualTo((true, true))); Toggle(shell, "Shrink").PerformClick(); @@ -183,32 +183,60 @@ public void GivenDefragment_WhenAnotherOperationIsPicked_ThenTheDefragModesAndLa public void GivenCarveHole_WhenPickedAndUnpicked_ThenItsSizeAndOffsetAppearOnlyWhilePicked() { WithImages(root => WithShell(shell => { OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); - var size = Field(shell, "_holeSizeItem"); - var at = Field(shell, "_holeAtItem"); - Assert.That((size.Visible, at.Visible), Is.EqualTo((false, false))); + var fields = new RibbonFieldItem[] { + Field(shell, "_holeSizeSpinner"), Field(shell, "_holePlacementCombo"), Field(shell, "_holeOffsetSpinner"), + }; + Assert.That(fields.Select(f => f.Visible), Is.All.False); Toggle(shell, "Carve Hole").PerformClick(); - Assert.That((size.Visible, at.Visible), Is.EqualTo((true, true))); + Assert.That(fields.Select(f => f.Visible), Is.All.True); Toggle(shell, "Re-order").PerformClick(); - Assert.That((size.Visible, at.Visible), Is.EqualTo((false, false))); + Assert.That(fields.Select(f => f.Visible), Is.All.False); })); } - [TestCase("0", false)] - [TestCase("1", true)] - [TestCase("256", true)] - [TestCase("257", false)] - [TestCase("two", false)] - public void GivenAnInterleave_WhenTyped_ThenStartFollowsWhetherItIsInRange(string stride, bool startable) { + [Test] + public void GivenCarveHole_WhenTheHoleIsPlacedAtTheEndOrAtAnOffset_ThenTheOffsetFieldFollowsAndTheEngineGetsTheBytes() { + WithImages(root => WithShell(shell => { + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + Toggle(shell, "Carve Hole").PerformClick(); + var placement = Field(shell, "_holePlacementCombo"); + var offset = Field(shell, "_holeOffsetSpinner"); + var presenter = Field(shell, "_presenter"); + Field(shell, "_holeSizeSpinner").Value = 128; + + Assert.That((offset.Enabled, presenter.BuildDefragOptions().HoleAt), Is.EqualTo((false, -1L)), "End: no offset, auto placement"); + + placement.SelectedIndex = 1; + offset.Value = 256; + var options = presenter.BuildDefragOptions(); + Assert.That((offset.Enabled, options.HoleSize, options.HoleAt), Is.EqualTo((true, 128L * 1024, 256L * 1024))); + })); + } + + /// + /// The interleave is a spinner over 1–256, so a value outside the range cannot be entered: it is + /// clamped, and Start stays available. The text-level boundaries are covered by the presenter tests. + /// + [TestCase(0, 1)] + [TestCase(1, 1)] + [TestCase(256, 256)] + [TestCase(257, 256)] + public void GivenTheInterleaveSpinner_WhenSet_ThenItClampsToOneTo256AndTheEngineGetsThatStride(int typed, int expected) { WithImages(root => WithShell(shell => { OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + var spinner = Field(shell, "_interleaveSpinner"); + spinner.Value = 7; // away from the boundary, so every case is a change - Field(shell, "_interleaveBox").Text = stride; + spinner.Value = typed; - var start = Item(shell, "Start"); - Assert.That(start.Enabled, Is.EqualTo(startable)); - if (!startable) Assert.That(start.ToolTipText, Does.Contain("Interleave"), "Start says what blocks it"); + var presenter = Field(shell, "_presenter"); + Assert.Multiple(() => { + Assert.That(spinner.Value, Is.EqualTo((decimal)expected)); + Assert.That(presenter.BuildDefragOptions().InterleaveStride, Is.EqualTo(expected)); + Assert.That(Item(shell, "Start").Enabled, Is.True); + }); })); } @@ -261,32 +289,21 @@ public void GivenAContextMenuVerb_WhenRun_ThenTheDefragmentTabOpensWithThatOpera } /// - /// A hosted field gets one stacked row. GTK draws a native combo box at its natural height - /// whatever bounds it is handed, which cut the bottom off its text, so every hosted input must - /// stay within its row and none may be a native combo box. + /// GTK draws a native combo box at its natural height whatever bounds it is handed, which cut the + /// bottom off its text in a ribbon row. Every input on the tab is therefore a ribbon field — drawn + /// by the ribbon and always one stacked row — and none is a hosted native control. /// [Test] - public void GivenTheOptionsFields_WhenTheTabIsLaidOut_ThenEveryHostedInputFitsItsRow() { + public void GivenTheDefragmentTab_WhenItsInputsAreCollected_ThenEachIsAOneRowRibbonFieldAndNoneIsAHostedControl() { WithImages(root => WithShell(shell => { OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); - Toggle(shell, "Carve Hole").PerformClick(); - var ribbon = Field(shell, "_ribbon"); - ribbon.PerformLayout(); - - var hosted = DefragTab(shell).Groups - .SelectMany(g => g.Items.Cast().OfType().Where(h => h.Visible).Select(h => (Group: g, Host: h))) - .ToList(); - Assert.That(hosted, Is.Not.Empty); + var items = DefragTab(shell).Groups.SelectMany(g => g.Items.Cast()).ToList(); Assert.Multiple(() => { - foreach (var (group, host) in hosted) { - var row = group.Bounds.Height / 3; - Assert.That(host.Control.Height, Is.LessThanOrEqualTo(row), $"{host.Text} is taller than a row"); - foreach (Control input in host.Control.Controls) { - Assert.That(input.Height, Is.LessThanOrEqualTo(host.Control.Height), $"{host.Text}: {input.GetType().Name}"); - Assert.That(input, Is.Not.InstanceOf(), $"{host.Text}: a native combo box ignores its row height on GTK"); - } - } + Assert.That(items.OfType(), Is.Empty, "a hosted native control ignores its row height on GTK"); + var fields = items.OfType().ToList(); + Assert.That(fields.Select(f => f.Text), Is.SupersetOf(new[] { "Interleave", "Metadata", "Profile", "Hole KiB", "Hole at", "Offset KiB" })); + Assert.That(fields.Select(f => f.ItemSize), Is.All.EqualTo(RibbonItemSize.Small), "one stacked row each"); }); })); } @@ -325,7 +342,7 @@ public void GivenARequestTheFormatRefuses_WhenStarted_ThenItIsReportedAndTheImag OpenOnDefragmentTab(shell, image); Toggle(shell, "Carve Hole").PerformClick(); - Field(shell, "_holeSizeBox").Text = "1g"; + Field(shell, "_holeSizeSpinner").Value = 1024 * 1024; // 1 GiB on a 1.44 MB floppy Item(shell, "Start").PerformClick(); Settle(shell); From 7a4eb59d02cbbfb2cbca8f387372271f642deb6f Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 2 Oct 2026 09:12:40 +0200 Subject: [PATCH 10/16] * the Defragment tab asks the registry's maintenance profile; Sort Entries sorts FAT and exFAT directories The tab's adapter guessed capabilities itself. It probed IArchiveDefragmentable with a stream that throws on first touch, and decided Optimize from a list of format ids. The registry now answers the question directly (MaintenanceCapabilities.Describe -> MaintenanceProfile). The adapter, renamed TargetCapabilities so it no longer shares the registry type's name, is now a view of that profile. - Defragment: offered for DefragmentExtents or SortDirectoryEntries. The extent strategies and the interleave, metadata and profile options follow the profile's DefragFeatures. Sort Entries runs MaintenanceOperations.SortDirectoryEntries and moves no data, so the layout options do not apply to it. - Optimize: Compress, Repack or Canonicalize, as offered, chosen in a Method combo. Each runs through MaintenanceOperations: staged, verified, replaced atomically. The ad-hoc routes (re-encode, 7z regrouping, CVF, chunk placement) are gone with the probe code. - Shrink, Purge, Clear, Scramble and Compact: run through the object that carries the capability (IArchiveShrinkable, IArchivePurgeable, IWipeEmpty). Minimal Geometry is gone because Compact now refuses it; changing geometry is its own operation, offered nowhere yet. Tests: the adapter agrees with the profile for every registered format. Sort Entries is offered for FAT and exFAT, not NTFS or ZIP. On an unsorted FAT volume it puts the root in name order with size and every file byte-identical, both through the presenter and from the ribbon. fsck.fat -n accepts the sorted volume (ExternalFsInterop, WSL). --- .../Maintenance/MaintenanceCapabilities.cs | 204 ---------------- .../Maintenance/MaintenancePresenter.cs | 220 ++++-------------- .../Maintenance/MaintenanceVerb.cs | 28 +-- .../Maintenance/TargetCapabilities.cs | 145 ++++++++++++ Compression.NativeUI/README.md | 15 +- .../ViewModels/MainViewModel.cs | 4 +- .../Views/MainForm.Defragment.cs | 48 ++-- .../NativeUI/MainFormTests.Defragment.cs | 43 +++- .../NativeUI/MaintenanceFixtures.cs | 27 +++ .../NativeUI/MaintenancePresenterTests.cs | 106 +++++++-- README.md | 5 +- 11 files changed, 397 insertions(+), 448 deletions(-) delete mode 100644 Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs create mode 100644 Compression.NativeUI/Maintenance/TargetCapabilities.cs diff --git a/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs b/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs deleted file mode 100644 index d2854bfe6..000000000 --- a/Compression.NativeUI/Maintenance/MaintenanceCapabilities.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Collections.Concurrent; -using Compression.Lib; -using Compression.Registry; -using Compression.Registry.Layout; - -namespace Compression.NativeUI.Maintenance; - -/// One answer to "can this target do that?": yes or no, and the sentence that says why. -internal readonly record struct Capability(bool Supported, string Reason) { - public static Capability Yes(string detail) => new(true, detail); - public static Capability No(string reason) => new(false, reason); -} - -/// -/// What one format offers the Defragment tab: which operations, which defragmentation strategies and -/// which layout options. The ribbon enables exactly what this says and nothing else, so an option is -/// never offered only to be ignored or refused. -/// -/// -/// This is the single place that turns the registry's capability interfaces into those answers. -/// When the registry grows a first-class capability model, this file is the one that changes. -/// -internal sealed record MaintenanceCapabilities( - string FormatId, - IReadOnlyDictionary Verbs, - IReadOnlyDictionary Strategies, - Capability PackAtStart, - Capability PackAtEnd, - Capability Interleave, - Capability MetadataZone, - Capability LayoutProfile, - Capability ChunkPlacement, - Capability MinimalGeometry, - OptimizeRoute OptimizeRoute, - bool StagedDefragment) { - - private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal); - - /// Whether any operation at all can run on the target. - public bool AnySupported => this.Verbs.Values.Any(v => v.Supported); - - public Capability Verb(MaintenanceVerb verb) => this.Verbs.TryGetValue(verb, out var c) ? c : Capability.No("Not offered."); - public Capability Strategy(DefragStrategy strategy) => this.Strategies.TryGetValue(strategy, out var c) ? c : Capability.No("Not offered."); - - /// - /// The capabilities of . Answers depend on the descriptor alone, so they - /// are worked out once per format and shared. - /// - public static MaintenanceCapabilities For(string formatId) { - ArgumentNullException.ThrowIfNull(formatId); - return Cache.GetOrAdd(formatId, Evaluate); - } - - private static MaintenanceCapabilities Evaluate(string formatId) { - var ops = FormatRegistry.GetArchiveOps(formatId); - var descriptor = FormatRegistry.GetById(formatId); - var name = descriptor?.DisplayName is { Length: > 0 } display ? display : formatId; - - var defragmentable = ops as IArchiveDefragmentable; - var modes = defragmentable is null ? [] : ProbeModes(defragmentable); - var ownOptionsPath = defragmentable is not null && DeclaresOwnOptionsPath(defragmentable); - - var route = OptimizeRouteOf(formatId, ops); - var verbs = new Dictionary { - [MaintenanceVerb.Defragment] = modes.Count > 0 - ? Capability.Yes($"Lays {name} out again; size and contents unchanged.") - : Capability.No($"{name} has no lossless defragmenter."), - [MaintenanceVerb.Optimize] = route switch { - OptimizeRoute.FileInternal => Capability.Yes("Reorders the chunks inside the file (e.g. fast-start)."), - OptimizeRoute.CompressedVolume => Capability.Yes("Re-encodes every cluster, keeping whichever of compressed or stored is smaller."), - OptimizeRoute.SolidBlocks => Capability.Yes("Tries five solid-block groupings and keeps the smallest; the original stays until the winner is committed."), - OptimizeRoute.Reencode => Capability.Yes("Re-encodes with the strongest settings into a staged copy, then replaces the original."), - _ => Capability.No(defragmentable is not null - ? $"{name} has no optimizer; Defragment is its layout operation." - : $"{name} has no optimizer."), - }, - [MaintenanceVerb.Shrink] = formatId is "Fat" or "Ext" or "Ext1" or "Vhd" || ops is IArchiveShrinkable - ? Capability.Yes("Minimises the stored footprint, keeping every file.") - : Capability.No($"{name} cannot shrink."), - [MaintenanceVerb.Purge] = ops is IArchiveModifiable - ? Capability.Yes("Erases every file, leaving a valid empty container. Asks first.") - : Capability.No($"{name} cannot remove entries, so it cannot be purged."), - [MaintenanceVerb.WipeEmpty] = ops is IWipeEmpty - ? Capability.Yes("Zeroes unused space — free clusters, slack, dead bytes — and nothing else.") - : Capability.No($"{name} exposes no layout to tell free space from live data."), - [MaintenanceVerb.Compact] = ops is IArchiveDefragmentable or IArchiveShrinkable or IArchiveCreatable - ? Capability.Yes("Defragment, optimize and shrink in one pass.") - : Capability.No($"{name} offers none of the steps Compact is made of."), - [MaintenanceVerb.Scramble] = ops is IFilesystemScrambleable - ? Capability.Yes("Scatters every block on purpose, so Defragment has something to undo. Contents preserved; asks first.") - : Capability.No($"{name} cannot scatter a volume in place."), - }; - - var start = modes.Contains(DefragMode.ConsolidateAtStart); - var end = modes.Contains(DefragMode.ConsolidateAtEnd); - var strategies = new Dictionary { - [DefragStrategy.Consolidate] = start || end - ? Capability.Yes("Packs every file at one end; free space ends up in one piece.") - : Capability.No($"{name} cannot pack its files together."), - [DefragStrategy.Defrag] = modes.Contains(DefragMode.FillHolesLazy) - ? Capability.Yes("Fills existing holes from the tail, moving as few bytes as possible.") - : Capability.No($"{name} cannot fill holes lazily."), - [DefragStrategy.Reorder] = modes.Contains(DefragMode.AscendingOrder) - ? Capability.Yes("Moves only what reads backwards, so every file's blocks ascend.") - : Capability.No($"{name} cannot reorder blocks into ascending order."), - // No descriptor offers a directory-entry sort through the registry yet, so the honest answer - // for every format is no. It is still a capability answer: the day a descriptor gains it, this - // is the line that turns it on. - [DefragStrategy.SortEntries] = Capability.No($"{name} does not offer sorting directory entries."), - [DefragStrategy.CarveHole] = modes.Contains(DefragMode.CarveHole) - ? Capability.Yes("Reserves one contiguous free region of the size you choose.") - : Capability.No($"{name} cannot carve a hole."), - }; - - var probeMode = modes.Count > 0 ? modes[0] : DefragMode.ConsolidateAtStart; - Capability Option(string what, DefragOptions options) { - if (defragmentable is null) return Capability.No($"{name} has no defragmenter."); - if (!ownOptionsPath) return Capability.No($"{name} defragments by rebuilding, which ignores {what}."); - return Accepts(defragmentable, options) ? Capability.Yes($"{name} honours {what}.") : Capability.No($"{name} refuses {what}."); - } - - return new( - formatId, - verbs, - strategies, - PackAtStart: start ? Capability.Yes("Packs at the front; free space ends up after the last file.") : Capability.No($"{name} cannot pack at the start."), - PackAtEnd: end ? Capability.Yes("Packs at the back instead; free space stays at the front.") : Capability.No($"{name} cannot pack at the end."), - Interleave: Option("block interleave", new() { Mode = probeMode, InterleaveStride = 2 }), - MetadataZone: Option("metadata placement", new() { Mode = probeMode, MetadataZonePlacement = Registry.MetadataZone.Front }), - LayoutProfile: Option("layout profiles", new() { Mode = probeMode, LayoutTemplate = new LayoutTemplate { Name = "probe" } }), - ChunkPlacement: ops is IFileInternalChunkMover - ? Capability.Yes("Where metadata chunks go relative to the payload.") - : Capability.No($"{name} has no chunk placement to choose."), - MinimalGeometry: ops is IArchiveCreatable and IFormatOptionsSchema - ? Capability.Yes("Rebuilds at the smallest geometry the format allows; the result may not be a standard image.") - : Capability.No($"{name} has no geometry to minimise."), - route, - StagedDefragment: defragmentable is not null && !DeclaresOwnOptionsPath(defragmentable) && !DeclaresOwnPlainPath(defragmentable)); - } - - private static OptimizeRoute OptimizeRouteOf(string formatId, IArchiveFormatOperations? ops) { - if (ops is IFileInternalChunkMover) return OptimizeRoute.FileInternal; - if (formatId is "DoubleSpace" or "DriveSpace" or "DriveSpace3") return OptimizeRoute.CompressedVolume; - if (formatId == "SevenZip") return OptimizeRoute.SolidBlocks; - - // ArchiveOperations.Optimize re-encodes these; anything else it copies through unchanged, which - // is not an operation worth offering. - if (formatId is "Zip" or "Gzip" or "Zlib" or "MacBinary") return OptimizeRoute.Reencode; - return Enum.TryParse(formatId, out var format) && FormatDetector.GetTarCompression(format).HasValue - ? OptimizeRoute.Reencode - : OptimizeRoute.None; - } - - // ── probing ───────────────────────────────────────────────────────────────────────────────── - - /// The modes the descriptor accepts, in order. - private static List ProbeModes(IArchiveDefragmentable defragmentable) - => [.. Enum.GetValues().Where(mode => Accepts(defragmentable, new() { Mode = mode }))]; - - /// - /// Asks the descriptor whether it takes , without touching any image. - /// There is no way to ask directly, so it is handed a stream that throws on first touch: reaching - /// the I/O means the request was accepted, while before any I/O - /// means it was refused — the guard every lossless defragmenter runs first. - /// - private static bool Accepts(IArchiveDefragmentable defragmentable, DefragOptions options) { - try { - using var probe = new ProbeStream(); - defragmentable.Defragment(probe, options); - return true; // Unlikely: the probe should have been touched first. - } catch (ProbeAbortException) { - return true; - } catch { - // NotSupportedException is the refusal; anything else is not taken as a yes either. - return false; - } - } - - /// - /// Whether the descriptor implements the options overload itself. The interface default only - /// knows and rebuilds, so it would accept layout options - /// without applying them; only a descriptor's own implementation can be asked about those. - /// - private static bool DeclaresOwnOptionsPath(IArchiveDefragmentable defragmentable) - => defragmentable.GetType().GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream), typeof(DefragOptions)]) is not null; - - private static bool DeclaresOwnPlainPath(IArchiveDefragmentable defragmentable) - => defragmentable.GetType().GetMethod(nameof(IArchiveDefragmentable.Defragment), [typeof(Stream)]) is not null; - - private sealed class ProbeAbortException : Exception; - - private sealed class ProbeStream : Stream { - public override bool CanRead => true; - public override bool CanSeek => true; - public override bool CanWrite => true; - public override long Length => 1024; - public override long Position { get => 0; set => throw new ProbeAbortException(); } - public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); - public override long Seek(long offset, SeekOrigin origin) => throw new ProbeAbortException(); - public override void SetLength(long value) => throw new ProbeAbortException(); - public override void Write(byte[] buffer, int offset, int count) => throw new ProbeAbortException(); - } -} diff --git a/Compression.NativeUI/Maintenance/MaintenancePresenter.cs b/Compression.NativeUI/Maintenance/MaintenancePresenter.cs index 2dd1febfc..0247a8053 100644 --- a/Compression.NativeUI/Maintenance/MaintenancePresenter.cs +++ b/Compression.NativeUI/Maintenance/MaintenancePresenter.cs @@ -35,14 +35,15 @@ internal sealed class MaintenancePresenter { public MaintenancePresenter(string imagePath, string formatId) { this.ImagePath = imagePath; this.FormatId = formatId; - this.Capabilities = MaintenanceCapabilities.For(formatId); + this.Capabilities = TargetCapabilities.For(formatId); this.Verb = VerbOrder.Cast().FirstOrDefault(v => this.Capabilities.Verb(v!.Value).Supported); this.Strategy = Enum.GetValues().FirstOrDefault(s => this.Capabilities.Strategy(s).Supported); + this.OptimizeMethod = this.Capabilities.OptimizeMethods.FirstOrDefault(); } public string ImagePath { get; } public string FormatId { get; } - public MaintenanceCapabilities Capabilities { get; } + public TargetCapabilities Capabilities { get; } public IArchiveFormatOperations? Operations => FormatRegistry.GetArchiveOps(this.FormatId); // ── what the user picked ──────────────────────────────────────────────────────────────────── @@ -56,8 +57,10 @@ public MaintenancePresenter(string imagePath, string formatId) { public string HoleSizeText { get; set; } = "64m"; public string HoleAtText { get; set; } = "auto"; public string SeedText { get; set; } = "1"; - public bool MinimalGeometry { get; set; } - public MetadataPlacementProfile? ChunkPlacement { get; set; } + public OptimizeMethod OptimizeMethod { get; set; } + + /// Whether the picked defragmentation moves extents, as against sorting directory entries. + public bool MovesExtents => this.Strategy != DefragStrategy.SortEntries; public bool IsRunning { get; private set; } @@ -71,6 +74,8 @@ public string? StartBlocker { if (this.Verb is not { } verb) return "Pick an operation."; if (this.Capabilities.Verb(verb) is { Supported: false } refused) return refused.Reason; if (verb == MaintenanceVerb.Defragment && this.Capabilities.Strategy(this.Strategy) is { Supported: false } strategy) return strategy.Reason; + if (verb == MaintenanceVerb.Optimize && !this.Capabilities.OptimizeMethods.Contains(this.OptimizeMethod)) + return $"This format does not offer {TargetCapabilities.Describe(this.OptimizeMethod)}."; return this.Validate().FirstOrDefault(); } } @@ -80,7 +85,7 @@ public IReadOnlyList Validate() { var errors = new List(); switch (this.Verb) { case MaintenanceVerb.Defragment: - if (this.Capabilities.Interleave.Supported && MaintenanceInput.ParseInterleave(this.InterleaveText).Error is { } stride) errors.Add(stride); + if (this.MovesExtents && this.Capabilities.Interleave.Supported && MaintenanceInput.ParseInterleave(this.InterleaveText).Error is { } stride) errors.Add(stride); if (this.Strategy == DefragStrategy.CarveHole) { if (MaintenanceInput.ParseSize(this.HoleSizeText).Error is { } size) errors.Add(size); if (MaintenanceInput.ParseHoleAt(this.HoleAtText).Error is { } at) errors.Add(at); @@ -95,22 +100,14 @@ public IReadOnlyList Validate() { return errors; } - /// Whether the picked operation can be stopped part-way. - public bool CanCancel => this.Verb switch { - MaintenanceVerb.Defragment => true, - MaintenanceVerb.Optimize => this.Capabilities.OptimizeRoute is OptimizeRoute.Reencode or OptimizeRoute.SolidBlocks, - _ => false, - }; + /// Whether the picked operation can be stopped part-way: only an extent move can. + public bool CanCancel => this.Verb == MaintenanceVerb.Defragment && this.MovesExtents; /// /// Whether stopping discards a staged copy (the original untouched) rather than leaving in-place - /// moves where they got to. + /// moves where they got to. Extent moves are always in place. /// - public bool IsStaged => this.Verb switch { - MaintenanceVerb.Defragment => this.Capabilities.StagedDefragment, - MaintenanceVerb.Optimize => true, - _ => false, - }; + public bool IsStaged => false; /// The engine mode the picked strategy stands for. public DefragMode EngineMode => this.Strategy switch { @@ -162,9 +159,6 @@ public DefragOptions BuildDefragOptions(Action? onProgress case MaintenanceVerb.Scramble: return $"Fragment {file} on purpose?\n\nEvery file's blocks are scattered across the volume. The contents are preserved exactly " + "-- only the layout changes, and Defragment undoes it -- but this is a testing tool, not a repair."; - case MaintenanceVerb.Compact when this.MinimalGeometry && this.Capabilities.MinimalGeometry.Supported: - return "Minimal geometry rebuilds the container at the smallest size the format allows (e.g. a 1.44 MB FAT floppy collapses to a few KB).\n\n" - + "Contents are preserved, but the result may no longer be a standard, mountable image. Continue?"; default: return null; } @@ -191,22 +185,22 @@ public Task RunAsync(Action progress, A // Everything the worker needs is read here, on the caller's thread, so a later ribbon change // cannot alter a run already under way. - var defragOptions = verb == MaintenanceVerb.Defragment ? this.BuildDefragOptions(ev => progress(FromEvent(ev)), token) : null; + var defragOptions = verb == MaintenanceVerb.Defragment && this.MovesExtents ? this.BuildDefragOptions(ev => progress(FromEvent(ev)), token) : null; var seed = MaintenanceInput.ParseSeed(this.SeedText).Value; - var minimal = this.MinimalGeometry && this.Capabilities.MinimalGeometry.Supported; - var placement = this.ChunkPlacement; + var method = this.OptimizeMethod; return Task.Run(() => { var clock = Stopwatch.StartNew(); MaintenanceOutcome outcome; try { outcome = verb switch { - MaintenanceVerb.Defragment => this.RunDefragment(path, defragOptions!, log), - MaintenanceVerb.Optimize => this.RunOptimize(path, placement, progress, log, token), + MaintenanceVerb.Defragment when defragOptions is null => this.RunSortEntries(path), + MaintenanceVerb.Defragment => this.RunDefragment(path, defragOptions, log), + MaintenanceVerb.Optimize => RunOptimize(path, method), MaintenanceVerb.Shrink => this.RunShrink(path), MaintenanceVerb.WipeEmpty => this.RunWipe(path), MaintenanceVerb.Purge => this.RunPurge(path), - MaintenanceVerb.Compact => RunCompact(path, minimal, log), + MaintenanceVerb.Compact => RunCompact(path, log), MaintenanceVerb.Scramble => this.RunScramble(path, seed, progress), _ => new(MaintenanceOutcomeKind.Refused, $"{verb} is not an operation.", false), }; @@ -242,8 +236,8 @@ private static MaintenanceProgress FromEvent(DefragProgressEvent ev) => new(ev.BlockMap, ev.ImageSize, ev.CurrentReadOffset, ev.CurrentWriteOffset, ev.Fraction, ev.Status, CommitStarted: ev.Phase == "committing"); private MaintenanceOutcome RunDefragment(string path, DefragOptions options, Action log) { - if (this.Operations is not IArchiveDefragmentable defragmentable) - throw new NotSupportedException($"{this.FormatId} has no defragmenter."); + if (TargetCapabilities.Carrier(this.FormatId, MaintenanceCapability.DefragmentExtents) is not IArchiveDefragmentable defragmentable) + throw new NotSupportedException($"{this.FormatId} does not move extents in place."); log($"Mode: {options.Mode}{(options.InterleaveStride > 1 ? $", interleave {options.InterleaveStride}" : "")}" + $"{(options.MetadataZonePlacement != MetadataZone.Unchanged ? $", metadata {options.MetadataZonePlacement}" : "")}" @@ -264,122 +258,31 @@ private MaintenanceOutcome RunDefragment(string path, DefragOptions options, Act return new(MaintenanceOutcomeKind.Succeeded, finalStatus ?? "Defragmented; size and contents unchanged.", true); } - private MaintenanceOutcome RunOptimize(string path, MetadataPlacementProfile? placement, Action progress, Action log, CancellationToken token) { - var originalSize = new FileInfo(path).Length; - switch (this.Capabilities.OptimizeRoute) { - case OptimizeRoute.FileInternal: { - if (this.Operations is not IFileInternalChunkMover mover) throw new NotSupportedException($"{this.FormatId} has no chunk mover."); - using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) - mover.Optimize(stream, placement); - return new(MaintenanceOutcomeKind.Succeeded, SizeChange(originalSize, new FileInfo(path).Length), true); - } - - case OptimizeRoute.CompressedVolume: { - var descriptor = FormatRegistry.GetById(this.FormatId) ?? throw new NotSupportedException($"{this.FormatId} is not registered."); - var result = CvfOptimizer.Optimize(path, descriptor); - log($"Re-encoded via {result.MethodUsed}; {result.FilesCompressed} cluster(s) compressed, {result.FilesStoredVerbatim} stored verbatim."); - return new(MaintenanceOutcomeKind.Succeeded, SizeChange(result.OriginalSize, result.OptimizedSize), true); - } - - case OptimizeRoute.SolidBlocks: - return this.RunSolidBlocks(path, originalSize, progress, log, token); - - case OptimizeRoute.Reencode: - return this.RunReencode(path, originalSize, progress, token); - - default: - throw new NotSupportedException($"{this.FormatId} has no optimizer."); - } + private MaintenanceOutcome RunSortEntries(string path) { + var before = new FileInfo(path).Length; + MaintenanceOperations.SortDirectoryEntries(path); + return new(MaintenanceOutcomeKind.Succeeded, $"Every directory is in name order; {MaintenanceInput.FormatSize(before)}, no data moved.", true); } - private MaintenanceOutcome RunReencode(string path, long originalSize, Action progress, CancellationToken token) { - var staged = path + ".opt.tmp"; - AtomicFileWriter.TryDelete(staged); - if (this.Operations is { } ops && BlockMapSnapshot.StagedProjection(path, ops, originalSize) is { } projection) - progress(new(projection, originalSize, 0, -1, 0, "Staged rebuild — green: source read; orange: staged bytes written.")); - - try { - var worker = Task.Run(() => ArchiveOperations.Optimize(path, staged, password: null), CancellationToken.None); - while (!worker.Wait(100)) { - var written = FindStagedOutputLength(staged); - var fraction = originalSize > 0 ? Math.Clamp((double)written / originalSize, 0, 0.95) : 0; - var span = Math.Max(1L, originalSize); - progress(new(null, originalSize, Math.Clamp((long)(fraction * span), 0, span - 1), written > 0 ? Math.Clamp(written, 0, span - 1) : -1, fraction, - token.IsCancellationRequested - ? "Cancellation pending — the current unit finishes, then the staged copy is discarded." - : $"Rebuilding a staged copy — {MaintenanceInput.FormatSize(written)} written; the original is unchanged.")); - } - - var result = worker.GetAwaiter().GetResult(); - token.ThrowIfCancellationRequested(); - progress(new(null, originalSize, -1, -1, 0.99, "Staged copy complete — committing.", CommitStarted: true)); - AtomicFileWriter.ReplaceTarget(staged, path); - return new(MaintenanceOutcomeKind.Succeeded, $"{result.EntriesOptimized} entries re-encoded; {SizeChange(originalSize, result.OptimizedSize)}", true); - } finally { - AtomicFileWriter.TryDelete(staged); - } - } + /// Optimizes through the registry's staged, verified file operation; the original is replaced atomically. + private static MaintenanceOutcome RunOptimize(string path, OptimizeMethod method) { + var result = method switch { + OptimizeMethod.Compress => MaintenanceOperations.Compress(path, path), + OptimizeMethod.Repack => MaintenanceOperations.Repack(path, path), + _ => MaintenanceOperations.Canonicalize(path, path), + }; - private MaintenanceOutcome RunSolidBlocks(string path, long originalSize, Action progress, Action log, CancellationToken token) { - FileFormat.SevenZip.SolidBlockOptimizer.OptimizeResult result; - using (var source = File.OpenRead(path)) - result = FileFormat.SevenZip.SolidBlockOptimizer.Optimize( - source, - maxTrials: 5, - onProgress: (index, total, name) => log($" Trying strategy {index + 1}/{total}: {name}..."), - onDetailedProgress: detail => { - var span = Math.Max(1L, originalSize); - var share = detail.Phase == "extracting" ? detail.BytesDone / Math.Max(1.0, detail.BytesTotal) : detail.Current / Math.Max(1.0, detail.Total); - var (fraction, read, write) = detail.Phase switch { - "extracting" => (0.30 * share, Math.Clamp((long)(share * span), 0, span - 1), -1L), - "strategy" => (0.30 + 0.10 * share, -1L, -1L), - "building" => (0.40 + 0.55 * share, -1L, Math.Clamp((long)(share * span), 0, span - 1)), - _ => (0.0, -1L, -1L), - }; - progress(new(null, originalSize, read, write, Math.Clamp(fraction, 0, 0.95), detail.Phase switch { - "extracting" => $"Reading source entry: {detail.Name}", - "strategy" => $"Planning solid grouping: {detail.Name}", - "building" => $"Building staged solid candidate: {detail.Name}", - _ => "Staged 7z regrouping", - })); - }, - cancellationToken: token); - - foreach (var trial in result.Trials) - log($" {trial.StrategyName}: {MaintenanceInput.FormatSize(trial.OutputSize)} ({trial.Elapsed.TotalMilliseconds:F0} ms)"); - - var newSize = (long)result.Data.Length; - if (newSize >= originalSize) - return new(MaintenanceOutcomeKind.Succeeded, "No grouping beat the original; the archive is unchanged.", false); - - progress(new(null, originalSize, -1, -1, 0.99, "Winning layout selected — committing.", CommitStarted: true)); - AtomicFileWriter.WriteAllBytesAtomic(path, result.Data); - return new(MaintenanceOutcomeKind.Succeeded, $"winner {result.WinningStrategy}; {SizeChange(originalSize, newSize)}", true); + return new(MaintenanceOutcomeKind.Succeeded, + result.Changed ? $"{method}: {SizeChange(result.OriginalSize, result.NewSize)}" : $"{method}: nothing to gain; the file is unchanged.", + result.Changed); } private MaintenanceOutcome RunShrink(string path) { - var originalSize = new FileInfo(path).Length; - switch (this.FormatId) { - case "Fat": { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileSystem.Fat.FatShrinkHelper.Shrink(stream); - return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); - } - case "Ext" or "Ext1": { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileSystem.Ext.ExtShrinkHelper.Shrink(stream); - return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); - } - case "Vhd": { - using var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite); - var result = FileFormat.Vhd.VhdCompactor.Compact(stream); - return Shrunk(result.WasReduced, result.OriginalSize, result.NewSize); - } - } - - if (this.Operations is not IArchiveShrinkable shrinkable) throw new NotSupportedException($"{this.FormatId} cannot shrink."); + if (TargetCapabilities.Carrier(this.FormatId, MaintenanceCapability.Shrink) is not IArchiveShrinkable shrinkable) + throw new NotSupportedException($"{this.FormatId} cannot shrink losslessly."); // Shrink into a staged file, then swap it in, so a crash mid-write cannot corrupt the source. + var originalSize = new FileInfo(path).Length; var staged = path + ".shrink.tmp"; try { using (var input = File.OpenRead(path)) @@ -387,18 +290,18 @@ private MaintenanceOutcome RunShrink(string path) { shrinkable.Shrink(input, output); var newSize = new FileInfo(staged).Length; + if (newSize >= originalSize) + return new(MaintenanceOutcomeKind.Succeeded, "Already compact; nothing to trim.", false); + AtomicFileWriter.ReplaceTarget(staged, path); - return Shrunk(newSize < originalSize, originalSize, newSize); + return new(MaintenanceOutcomeKind.Succeeded, SizeChange(originalSize, newSize), true); } finally { AtomicFileWriter.TryDelete(staged); } - - static MaintenanceOutcome Shrunk(bool reduced, long before, long after) - => new(MaintenanceOutcomeKind.Succeeded, reduced ? SizeChange(before, after) : "Already compact; nothing to trim.", reduced); } private MaintenanceOutcome RunWipe(string path) { - var ops = this.Operations; + var ops = TargetCapabilities.Carrier(this.FormatId, MaintenanceCapability.WipeUnused); var totalUnused = -1L; long wiped; using (var stream = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) { @@ -424,21 +327,17 @@ private MaintenanceOutcome RunWipe(string path) { } private MaintenanceOutcome RunPurge(string path) { - var ops = this.Operations; - if (ops is not IArchiveModifiable) throw new NotSupportedException($"{this.FormatId} cannot remove entries."); - - List entries; - using (var probe = File.OpenRead(path)) - entries = ops.List(probe, password: null); - if (entries.Count == 0) return new(MaintenanceOutcomeKind.Succeeded, "The container is already empty.", false); + if (TargetCapabilities.Carrier(this.FormatId, MaintenanceCapability.Purge) is not IArchivePurgeable purgeable) + throw new NotSupportedException($"{this.FormatId} has no valid empty state to purge to."); var originalSize = new FileInfo(path).Length; - ArchiveOperations.Remove(path, [.. entries.Select(e => e.Name)]); - return new(MaintenanceOutcomeKind.Succeeded, $"{entries.Count(e => !e.IsDirectory)} file(s) erased; {SizeChange(originalSize, new FileInfo(path).Length)}", true); + using (var image = File.Open(path, FileMode.Open, FileAccess.ReadWrite)) + purgeable.Purge(image); + return new(MaintenanceOutcomeKind.Succeeded, $"Every file erased; {SizeChange(originalSize, new FileInfo(path).Length)}", true); } - private static MaintenanceOutcome RunCompact(string path, bool minimal, Action log) { - var result = CompactOperation.Compact(path, new CompactOperation.CompactOptions { Minimal = minimal, Log = line => log(" " + line) }); + private static MaintenanceOutcome RunCompact(string path, Action log) { + var result = CompactOperation.Compact(path, new CompactOperation.CompactOptions { Log = line => log(" " + line) }); var steps = result.StepsRun.Count > 0 ? string.Join(", ", result.StepsRun) : "none"; return new(MaintenanceOutcomeKind.Succeeded, $"steps: {steps}; {SizeChange(result.OriginalSize, result.NewSize)}", result.StepsRun.Count > 0); } @@ -455,21 +354,4 @@ private static string SizeChange(long before, long after) { var pct = before > 0 ? 100.0 * (after - before) / before : 0; return $"{MaintenanceInput.FormatSize(before)} → {MaintenanceInput.FormatSize(after)} ({pct:+0.0;-0.0;0.0}%)"; } - - /// - /// The optimizer writes through its own temporary file, so the staged size is the largest of the - /// target and any sibling scratch file it is currently filling. - /// - private static long FindStagedOutputLength(string target) { - try { - var best = File.Exists(target) ? new FileInfo(target).Length : 0L; - var directory = Path.GetDirectoryName(target); - if (string.IsNullOrEmpty(directory)) directory = Directory.GetCurrentDirectory(); - foreach (var candidate in Directory.EnumerateFiles(directory, Path.GetFileName(target) + ".tmp.*")) - best = Math.Max(best, new FileInfo(candidate).Length); - return best; - } catch { - return 0; - } - } } diff --git a/Compression.NativeUI/Maintenance/MaintenanceVerb.cs b/Compression.NativeUI/Maintenance/MaintenanceVerb.cs index abc115dfd..bf05d67f4 100644 --- a/Compression.NativeUI/Maintenance/MaintenanceVerb.cs +++ b/Compression.NativeUI/Maintenance/MaintenanceVerb.cs @@ -32,8 +32,8 @@ internal enum MaintenanceVerb { /// /// How a defragmentation lays the volume out, in the words the ribbon uses. Each maps onto one -/// engine , except , which no -/// descriptor offers yet and is answered by the capability query rather than by a placeholder. +/// engine , except , which is +/// the registry's separate directory sort and moves no data. /// internal enum DefragStrategy { /// Pack every file at one end of the volume (ConsolidateAtStart, or ConsolidateAtEnd). @@ -45,27 +45,21 @@ internal enum DefragStrategy { /// Move only what reads backwards, so every file ascends (AscendingOrder). Reorder, - /// Sort the entries of every directory. No descriptor offers this today. + /// Sort the entries of every directory by name, in place (SortDirectoryEntries). SortEntries, /// Reserve one contiguous free region of a chosen size (CarveHole). CarveHole, } -/// Which engine an optimize runs through for a given format. -internal enum OptimizeRoute { - /// The format has no optimizer. - None, +/// How Optimize improves a container; the format offers some, all or none of them. +internal enum OptimizeMethod { + /// Re-encode the payload with the best compression, same format. + Compress, - /// Chunks inside one file are reordered in place (MP4 fast-start, PNG/JPEG metadata). - FileInternal, + /// Rebuild from the same entries, stored bytes copied verbatim, dead space dropped. + Repack, - /// A compressed-volume file's clusters are re-encoded (DoubleSpace, DriveSpace). - CompressedVolume, - - /// A 7z archive's solid blocks are regrouped; the smallest candidate wins. - SolidBlocks, - - /// The container is re-encoded with its strongest settings into a staged copy. - Reencode, + /// Rewrite into the canonical representation (chunk order, padding, header normal form). + Canonicalize, } diff --git a/Compression.NativeUI/Maintenance/TargetCapabilities.cs b/Compression.NativeUI/Maintenance/TargetCapabilities.cs new file mode 100644 index 000000000..4c89c080f --- /dev/null +++ b/Compression.NativeUI/Maintenance/TargetCapabilities.cs @@ -0,0 +1,145 @@ +using System.Collections.Concurrent; +using Compression.Registry; + +namespace Compression.NativeUI.Maintenance; + +/// One answer to "can this target do that?": yes or no, and the sentence that says why. +internal readonly record struct Capability(bool Supported, string Reason) { + public static Capability Yes(string detail) => new(true, detail); + public static Capability No(string reason) => new(false, reason); +} + +/// +/// What one format offers the Defragment tab: which operations, which defragmentation strategies, +/// which layout options and which ways of optimizing. The ribbon enables exactly what this says +/// and nothing else, so an option is never offered only to be ignored or refused. +/// +/// +/// This is the single place that turns the registry's answer — +/// , a — into +/// the ribbon's terms. The owner's names map onto the registry's operations as: Defragment → +/// (and Sort Entries → +/// ), Optimize → Compress, Repack or +/// Canonicalize as offered, Clear → , and Shrink, Purge, +/// Compact and Scramble by their own names. +/// +internal sealed record TargetCapabilities( + MaintenanceProfile Profile, + IReadOnlyDictionary Verbs, + IReadOnlyDictionary Strategies, + Capability PackAtStart, + Capability PackAtEnd, + Capability Interleave, + Capability MetadataZone, + Capability LayoutProfile, + IReadOnlyList OptimizeMethods) { + + /// Whether any operation at all can run on the target. + public bool AnySupported => this.Verbs.Values.Any(v => v.Supported); + + public Capability Verb(MaintenanceVerb verb) => this.Verbs.TryGetValue(verb, out var c) ? c : Capability.No("Not offered."); + public Capability Strategy(DefragStrategy strategy) => this.Strategies.TryGetValue(strategy, out var c) ? c : Capability.No("Not offered."); + + private static readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal); + + /// + /// The capabilities of the format registered as . They depend on the + /// descriptor alone, and the shell asks on every command requery, so they are worked out once. + /// + public static TargetCapabilities For(string formatId) { + ArgumentNullException.ThrowIfNull(formatId); + return Cache.GetOrAdd(formatId, Evaluate); + } + + private static TargetCapabilities Evaluate(string formatId) { + var descriptor = FormatRegistry.GetById(formatId); + var profile = descriptor is null + ? new MaintenanceProfile(formatId, MaintenanceCapability.None, DefragFeature.None, []) + : MaintenanceCapabilities.Describe(descriptor); + return From(profile, descriptor?.DisplayName is { Length: > 0 } display ? display : formatId); + } + + /// Turns a registry profile into the ribbon's answers. + internal static TargetCapabilities From(MaintenanceProfile profile, string name) { + bool Has(MaintenanceCapability capability) => profile.Supports(capability); + bool Honours(DefragFeature feature) => profile.Supports(feature); + + var extents = Has(MaintenanceCapability.DefragmentExtents); + var sort = Has(MaintenanceCapability.SortDirectoryEntries); + OptimizeMethod[] methods = [ + .. Has(MaintenanceCapability.Compress) ? [OptimizeMethod.Compress] : Array.Empty(), + .. Has(MaintenanceCapability.Repack) ? [OptimizeMethod.Repack] : Array.Empty(), + .. Has(MaintenanceCapability.Canonicalize) ? [OptimizeMethod.Canonicalize] : Array.Empty(), + ]; + + var verbs = new Dictionary { + [MaintenanceVerb.Defragment] = extents || sort + ? Capability.Yes(extents ? $"Moves {name}'s extents in place; size and contents unchanged." : $"Sorts {name}'s directory entries in place.") + : Capability.No($"{name} neither moves extents in place nor sorts directory entries."), + [MaintenanceVerb.Optimize] = methods.Length > 0 + ? Capability.Yes($"Optimizes by {string.Join(", ", methods.Select(Describe))}; the original is replaced only by a verified result.") + : Capability.No($"{name} has no lossless compress, repack or canonical form."), + [MaintenanceVerb.Shrink] = Has(MaintenanceCapability.Shrink) + ? Capability.Yes("Reduces the container to what it needs, keeping every file.") + : Capability.No($"{name} cannot shrink losslessly."), + [MaintenanceVerb.Purge] = Has(MaintenanceCapability.Purge) + ? Capability.Yes("Erases every file, leaving a valid empty container. Asks first.") + : Capability.No($"{name} has no valid empty state to purge to."), + [MaintenanceVerb.WipeEmpty] = Has(MaintenanceCapability.WipeUnused) + ? Capability.Yes("Zeroes unused space — free clusters, slack, dead bytes — and nothing else.") + : Capability.No($"{name} exposes no layout to tell free space from live data."), + [MaintenanceVerb.Compact] = Has(MaintenanceCapability.Compact) + ? Capability.Yes("Defragment extents, compress and shrink in one pass, each where offered.") + : Capability.No($"{name} offers none of the steps Compact is made of."), + [MaintenanceVerb.Scramble] = Has(MaintenanceCapability.Scramble) + ? Capability.Yes("Scatters every block on purpose, so Defragment has something to undo. Contents preserved; asks first.") + : Capability.No($"{name} cannot scatter a volume in place."), + }; + + Capability Mode(DefragFeature feature, string yes, string what) + => extents && Honours(feature) ? Capability.Yes(yes) : Capability.No($"{name} cannot {what}."); + + var start = extents && Honours(DefragFeature.ConsolidateAtStart); + var end = extents && Honours(DefragFeature.ConsolidateAtEnd); + var strategies = new Dictionary { + [DefragStrategy.Consolidate] = start || end + ? Capability.Yes("Packs every file at one end; free space ends up in one piece.") + : Capability.No($"{name} cannot pack its files together."), + [DefragStrategy.Defrag] = Mode(DefragFeature.FillHolesLazy, "Fills existing holes from the tail, moving as few bytes as possible.", "fill holes lazily"), + [DefragStrategy.Reorder] = Mode(DefragFeature.AscendingOrder, "Moves only what reads backwards, so every file's blocks ascend.", "reorder blocks into ascending order"), + [DefragStrategy.SortEntries] = sort + ? Capability.Yes("Sorts every directory's entries by name in place; no data moves.") + : Capability.No($"{name} does not offer sorting directory entries."), + [DefragStrategy.CarveHole] = Mode(DefragFeature.CarveHole, "Reserves one contiguous free region of the size you choose.", "carve a hole"), + }; + + return new( + profile, + verbs, + strategies, + PackAtStart: Mode(DefragFeature.ConsolidateAtStart, "Packs at the front; free space ends up after the last file.", "pack at the start"), + PackAtEnd: Mode(DefragFeature.ConsolidateAtEnd, "Packs at the back instead; free space stays at the front.", "pack at the end"), + Interleave: Mode(DefragFeature.Interleave, $"{name} honours block interleave.", "interleave blocks"), + MetadataZone: Mode(DefragFeature.MetadataZone, $"{name} honours metadata placement.", "place metadata"), + LayoutProfile: Mode(DefragFeature.LayoutTemplate, $"{name} honours layout profiles.", "follow a layout profile"), + methods); + } + + internal static string Describe(OptimizeMethod method) => method switch { + OptimizeMethod.Compress => "re-encoding with the best compression", + OptimizeMethod.Repack => "repacking without dead space", + _ => "rewriting into canonical form", + }; + + /// + /// The object that carries for : the + /// descriptor, or its archive or stream operations object. + /// + internal static object? Carrier(string formatId, MaintenanceCapability capability) { + var descriptor = FormatRegistry.GetById(formatId); + foreach (var candidate in new object?[] { descriptor, FormatRegistry.GetArchiveOps(formatId), FormatRegistry.GetStreamOps(formatId) }) + if (candidate is not null && MaintenanceCapabilities.Of(candidate).HasFlag(capability)) + return candidate; + return null; + } +} diff --git a/Compression.NativeUI/README.md b/Compression.NativeUI/README.md index bd494e027..d860a5fe2 100644 --- a/Compression.NativeUI/README.md +++ b/Compression.NativeUI/README.md @@ -58,15 +58,16 @@ backends, so Windows and Linux run the same code rather than two frontends drift | Group | Items | | --- | --- | - | Operation | Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Start; Stop where the operation can be stopped | - | Defrag Mode | Consolidate (Pack at End as its variant), Defrag (fill holes), Re-order (ascending blocks), Sort Entries, Carve Hole with its size and offset fields | - | Options | Block interleave (1–256), metadata placement, layout profile and Edit Profiles; the seed for Scramble, the chunk placement for a file-internal Optimize and Minimal Geometry for Compact appear while their operation is picked | + | Operation | Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Start; Stop while extents are being moved | + | Defrag Mode | Consolidate (Pack at End as its variant), Defrag (fill holes), Re-order (ascending blocks), Carve Hole with its size, placement and offset fields — all extent moves — and Sort Entries, which sorts every directory by name in place and moves no data | + | Options | Block interleave (spinner, 1–256), metadata placement, layout profile and Edit Profiles for extent moves; the seed for Scramble and the method for Optimize (Compress, Repack or Canonicalize, as the format offers) appear while their operation is picked | | View | Blocks, Circle, 3D Stack; Files panel; Legend; Analyze (read the layout again) | - Every item asks one capability adapter (`Maintenance/MaintenanceCapabilities.cs`) whether the target - supports it, and is disabled with the reason in its tooltip when not — Sort Entries, for instance, - says that no format offers it yet. Defrag modes and layout options are probed against the - descriptor's own refusal guard without touching the image. Operations run off the UI thread through + Every item asks one adapter (`Maintenance/TargetCapabilities.cs`) whether the target supports it, + and is disabled with the reason in its tooltip when not. The adapter is a view of the registry's + `MaintenanceCapabilities.Describe` profile — operations, honoured defrag features — so the tab, the + CLI and the support matrices cannot disagree; a test holds it to the profile for every registered + format. Sort Entries is offered for FAT and exFAT. Operations run off the UI thread through `MaintenancePresenter`, driving the map live; a refusal (`NotSupportedException`) is reported and leaves the image byte-identical, and after a change the archive is re-listed in place. A nested archive is maintained as a temporary copy and written back into its host after each change. diff --git a/Compression.NativeUI/ViewModels/MainViewModel.cs b/Compression.NativeUI/ViewModels/MainViewModel.cs index be9a0f11e..ce59f01f4 100644 --- a/Compression.NativeUI/ViewModels/MainViewModel.cs +++ b/Compression.NativeUI/ViewModels/MainViewModel.cs @@ -876,11 +876,11 @@ private bool TryResolveMaintenanceTarget(out string formatId, out ArchiveEntryVi /// offered for an operation the tab would then refuse. /// private bool CanMaintain(MaintenanceVerb verb) - => TryResolveMaintenanceTarget(out var formatId, out _) && MaintenanceCapabilities.For(formatId).Verb(verb).Supported; + => TryResolveMaintenanceTarget(out var formatId, out _) && TargetCapabilities.For(formatId).Verb(verb).Supported; /// Whether there is a maintenance target that supports at least one operation. internal bool HasMaintenanceTarget - => TryResolveMaintenanceTarget(out var formatId, out _) && MaintenanceCapabilities.For(formatId).AnySupported; + => TryResolveMaintenanceTarget(out var formatId, out _) && TargetCapabilities.For(formatId).AnySupported; /// /// Identifies the resolved maintenance target — the selected archive file or entry, else the open diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index e46f07157..eb89abb62 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -33,7 +33,6 @@ internal sealed partial class MainForm { private readonly Dictionary _strategyToggles = []; private readonly Dictionary _viewToggles = []; private RibbonToggleButton _packAtEndToggle = null!; - private RibbonToggleButton _minimalGeometryToggle = null!; private RibbonToggleButton _filesPanelToggle = null!; private RibbonToggleButton _legendToggle = null!; private RibbonButton _startButton = null!; @@ -52,7 +51,7 @@ internal sealed partial class MainForm { private readonly RibbonComboBox _holePlacementCombo = new("Hole at") { FieldWidth = 80 }; private readonly RibbonSpinner _holeOffsetSpinner = new("Offset KiB") { Minimum = 0, Maximum = 1L << 32, Value = 0, Increment = 64, FieldWidth = 80 }; private readonly RibbonSpinner _seedSpinner = new("Seed") { Minimum = int.MinValue, Maximum = int.MaxValue, Value = 1, FieldWidth = 80 }; - private readonly RibbonComboBox _placementCombo = new("Chunks") { FieldWidth = 112 }; + private readonly RibbonComboBox _optimizeMethodCombo = new("Method") { FieldWidth = 112 }; private readonly List _layoutProfileEntries = []; private MaintenanceSession? _maintenanceSession; @@ -167,22 +166,14 @@ private RibbonGroup OptionsGroup() { this.RefreshLayoutProfiles(); }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); - this._placementCombo.Items.AddRange(["Default", "Metadata first", "Data first"]); - this._placementCombo.SelectedIndex = 0; this.Field(this._seedSpinner, "Seeds Scramble's shuffle. The same seed deals the same layout every run.", () => this.WithPresenter(p => p.SeedText = SeedText())); - this.Field(this._placementCombo, "Where an optimized file puts its metadata chunks relative to the payload.", - () => this.WithPresenter(p => p.ChunkPlacement = this._placementCombo.SelectedIndex switch { - 1 => MetadataPlacementProfile.MetadataFirst, - 2 => MetadataPlacementProfile.DataFirst, - _ => null, - })); - this._minimalGeometryToggle = this.RibbonToggle("Minimal Geometry", IconKeys.Defragment, RibbonItemSize.Small, Keys.None, - "Compact rebuilds at the smallest geometry the format allows", @checked: false, on => this.WithPresenter(p => p.MinimalGeometry = on)); + this.Field(this._optimizeMethodCombo, "How Optimize improves the container: compress (best compression, same format), repack (same entries, dead space dropped) or canonicalize (normal form). Only what the format offers is listed.", + () => this.WithPresenter(p => p.OptimizeMethod = this.SelectedOptimizeMethod())); var group = new RibbonGroup("Options"); group.Items.AddRange(this._interleaveSpinner, this._metadataZoneCombo, this._layoutProfileCombo, - this._editProfilesButton, this._seedSpinner, this._placementCombo, this._minimalGeometryToggle); + this._editProfilesButton, this._seedSpinner, this._optimizeMethodCombo); return group; } @@ -338,6 +329,11 @@ private void ChooseView(BlockMapView view) { this.SyncDefragmentRibbon(); } + private OptimizeMethod SelectedOptimizeMethod() + => this._presenter?.Capabilities.OptimizeMethods is { } methods && this._optimizeMethodCombo.SelectedIndex is var i && i >= 0 && i < methods.Count + ? methods[i] + : OptimizeMethod.Compress; + private MetadataZone SelectedMetadataZone() => this._metadataZoneCombo.SelectedIndex switch { 1 => MetadataZone.Front, 2 => MetadataZone.Back, @@ -438,22 +434,26 @@ private void SyncDefragmentRibbon() { } this._holeOffsetSpinner.Enabled = idle && this._holePlacementCombo.SelectedIndex == 1; - Option(this._interleaveSpinner, idle && defrag, caps?.Interleave, defrag); - Option(this._metadataZoneCombo, idle && defrag, caps?.MetadataZone, defrag); - Option(this._layoutProfileCombo, idle && defrag, caps?.LayoutProfile, defrag); - this._editProfilesButton.Enabled = idle && defrag && caps?.LayoutProfile.Supported == true; + // The layout options shape extent moves; a directory sort moves nothing. + var extents = defrag && p!.MovesExtents; + Option(this._interleaveSpinner, idle && extents, caps?.Interleave, extents, appliesTo: "extent moves"); + Option(this._metadataZoneCombo, idle && extents, caps?.MetadataZone, extents, appliesTo: "extent moves"); + Option(this._layoutProfileCombo, idle && extents, caps?.LayoutProfile, extents, appliesTo: "extent moves"); + this._editProfilesButton.Enabled = idle && extents && caps?.LayoutProfile.Supported == true; var scramble = p?.Verb == MaintenanceVerb.Scramble; this._seedSpinner.Visible = scramble; - this._placementCombo.Visible = p?.Verb == MaintenanceVerb.Optimize && caps?.ChunkPlacement.Supported == true; - this._minimalGeometryToggle.Visible = p?.Verb == MaintenanceVerb.Compact && caps?.MinimalGeometry.Supported == true; Option(this._seedSpinner, idle && scramble, scramble ? Capability.Yes("Seeds Scramble's shuffle.") : null, scramble, appliesTo: "Scramble"); var optimize = p?.Verb == MaintenanceVerb.Optimize; - Option(this._placementCombo, idle && optimize, caps?.ChunkPlacement, optimize, appliesTo: "Optimize"); - var compact = p?.Verb == MaintenanceVerb.Compact; - this._minimalGeometryToggle.Enabled = idle && compact && caps?.MinimalGeometry.Supported == true; - this._minimalGeometryToggle.Checked = p?.MinimalGeometry == true && compact; - this._minimalGeometryToggle.ToolTipText = $"Minimal Geometry: {(caps is null ? NoTarget : !compact ? "Applies to Compact." : caps.MinimalGeometry.Reason)}"; + this._optimizeMethodCombo.Visible = optimize; + var methods = caps?.OptimizeMethods ?? []; + if (!this._optimizeMethodCombo.Items.Select(i => i).SequenceEqual(methods.Select(m => m.ToString()))) { + this._optimizeMethodCombo.Items.Clear(); + this._optimizeMethodCombo.Items.AddRange(methods.Select(m => m.ToString())); + } + this._optimizeMethodCombo.SelectedIndex = p is null ? -1 : methods.ToList().IndexOf(p.OptimizeMethod); + this._optimizeMethodCombo.Enabled = idle && optimize && methods.Count > 1; + this._optimizeMethodCombo.ToolTipText = $"Method: {(caps is null ? NoTarget : caps.Verb(MaintenanceVerb.Optimize).Reason)} {this._optimizeMethodCombo.Tag}"; var blocker = p?.StartBlocker ?? (p is null ? NoTarget : null); this._startButton.Enabled = idle && blocker is null; diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs index 7e7790df9..4b8dedba8 100644 --- a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -3,6 +3,7 @@ using System.Linq; using Compression.Core.Layout; using Compression.Lib; +using Compression.Registry; using Compression.NativeUI; using Compression.NativeUI.Controls; using Compression.NativeUI.Navigation; @@ -122,7 +123,7 @@ public void GivenTheBrowser_WhenTheDefragmentTabIsEnteredAndLeft_ThenTheBlockVie // ── what is offered ───────────────────────────────────────────────────────────────────────── [Test] - public void GivenAFatImage_WhenOnTheTab_ThenDefragmentIsOfferedAndOptimizeIsNotAndEachSaysWhy() { + public void GivenAFatImage_WhenOnTheTab_ThenEachItemFollowsTheRegistryProfileAndSaysWhy() { WithImages(root => WithShell(shell => { OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); @@ -131,10 +132,11 @@ public void GivenAFatImage_WhenOnTheTab_ThenDefragmentIsOfferedAndOptimizeIsNotA Assert.That(Toggle(shell, "Defragment").Checked, Is.True, "the first supported operation is preselected"); Assert.That(Toggle(shell, "Shrink").Enabled, Is.True); Assert.That(Toggle(shell, "Clear").Enabled, Is.True); - Assert.That(Toggle(shell, "Optimize").Enabled, Is.False); - Assert.That(Toggle(shell, "Optimize").ToolTipText, Does.Contain("no optimizer")); - Assert.That(Toggle(shell, "Sort Entries").Enabled, Is.False); - Assert.That(Toggle(shell, "Sort Entries").ToolTipText, Does.Contain("sorting directory entries")); + var profile = MaintenanceCapabilities.Describe("Fat")!; + var optimizes = profile.Supports(MaintenanceCapability.Compress) || profile.Supports(MaintenanceCapability.Repack) || profile.Supports(MaintenanceCapability.Canonicalize); + Assert.That(Toggle(shell, "Optimize").Enabled, Is.EqualTo(optimizes)); + Assert.That(Toggle(shell, "Optimize").ToolTipText, Does.StartWith("Optimize: "), "says why either way"); + Assert.That(Toggle(shell, "Sort Entries").Enabled, Is.True, "FAT sorts directory entries (#440)"); Assert.That(Item(shell, "Start").Enabled, Is.True); }); })); @@ -149,6 +151,8 @@ public void GivenAZip_WhenOnTheTab_ThenDefragmentIsNotOfferedButOptimizeAndPurge Assert.That(Toggle(shell, "Defragment").Enabled, Is.False); Assert.That(Toggle(shell, "Optimize").Enabled, Is.True); Assert.That(Toggle(shell, "Optimize").Checked, Is.True); + Assert.That(Item(shell, "Method").Visible, Is.True, "Optimize offers its methods"); + Assert.That(Item(shell, "Method").Items, Is.Not.Empty); Assert.That(Toggle(shell, "Purge").Enabled, Is.True); Assert.That(Toggle(shell, "Consolidate").Enabled, Is.False, "no defrag mode applies to an archive"); }); @@ -334,6 +338,35 @@ public void GivenAFragmentedFat_WhenStartIsClicked_ThenItIsDefragmentedInPlaceWi })); } + [Test] + public void GivenAnUnsortedFat_WhenSortEntriesIsStarted_ThenTheRootIsSortedInPlaceAndTheLayoutOptionsDoNotApply() { + WithImages(root => WithShell(shell => { + var image = MaintenanceFixtures.UnsortedFat(root); + var size = new FileInfo(image).Length; + OpenOnDefragmentTab(shell, image); + + Toggle(shell, "Sort Entries").PerformClick(); + Assert.Multiple(() => { + Assert.That(Toggle(shell, "Sort Entries").Checked, Is.True); + Assert.That(Field(shell, "_interleaveSpinner").Enabled, Is.False, "a sort moves no data, so interleave does not apply"); + Assert.That(Field(shell, "_interleaveSpinner").ToolTipText, Does.Contain("extent moves")); + }); + + Item(shell, "Start").PerformClick(); + Settle(shell); + + var order = MaintenanceFixtures.RootOrder(image); + Assert.Multiple(() => { + Assert.That(Field(shell, "_model").StatusText, Does.StartWith("Defragment finished")); + Assert.That(order, Is.EqualTo(MaintenanceFixtures.NameOrder(order))); + Assert.That(new FileInfo(image).Length, Is.EqualTo(size)); + var after = MaintenanceFixtures.ReadFatFiles(image); + foreach (var (name, data) in MaintenanceFixtures.UnsortedFatFiles) + Assert.That(after[name], Is.EqualTo(data), name); + }); + })); + } + [Test] public void GivenARequestTheFormatRefuses_WhenStarted_ThenItIsReportedAndTheImageIsByteIdentical() { WithImages(root => WithShell(shell => { diff --git a/Compression.Tests/NativeUI/MaintenanceFixtures.cs b/Compression.Tests/NativeUI/MaintenanceFixtures.cs index bb9b4739f..f6c069be7 100644 --- a/Compression.Tests/NativeUI/MaintenanceFixtures.cs +++ b/Compression.Tests/NativeUI/MaintenanceFixtures.cs @@ -40,6 +40,33 @@ public static string FragmentedFat(string folder, string name = "volume.img", IR return path; } + /// Files written to the root out of name order, long names and 8.3 names mixed. + public static readonly IReadOnlyDictionary UnsortedFatFiles = new Dictionary { + ["Zulu long file name.txt"] = Pattern(6_000, 0x61), + ["alpha.txt"] = Pattern(1_200, 0x62), + ["Mike.bin"] = Pattern(9_000, 0x63), + ["BETA.TXT"] = Pattern(700, 0x64), + }; + + /// A FAT12 floppy whose root directory is out of name order. + public static string UnsortedFat(string folder, string name = "unsorted.img") { + var writer = new FileSystem.Fat.FatWriter(); + foreach (var (file, data) in UnsortedFatFiles) writer.AddFile(file, data); + var path = Path.Combine(folder, name); + File.WriteAllBytes(path, writer.Build()); + return path; + } + + /// The root directory's file names in the order the directory holds them. + public static List RootOrder(string path) { + using var stream = File.OpenRead(path); + var reader = new FileSystem.Fat.FatReader(stream, leaveOpen: true); + return [.. reader.Entries.Where(e => !e.IsDirectory && !e.Name.Contains('/')).Select(e => e.Name)]; + } + + public static List NameOrder(IEnumerable names) + => [.. names.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ThenBy(n => n, StringComparer.Ordinal)]; + public static string Zip(string folder, string name = "bundle.zip") { var path = Path.Combine(folder, name); using var archive = ZipFile.Open(path, ZipArchiveMode.Create); diff --git a/Compression.Tests/NativeUI/MaintenancePresenterTests.cs b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs index c31694a2e..4ce2712a0 100644 --- a/Compression.Tests/NativeUI/MaintenancePresenterTests.cs +++ b/Compression.Tests/NativeUI/MaintenancePresenterTests.cs @@ -32,43 +32,45 @@ public void TearDown() { // ── capabilities ──────────────────────────────────────────────────────────────────────────── [Test] - public void GivenFat_WhenCapabilitiesAreAsked_ThenDefragmentAndItsInPlaceModesAreOffered() { - var caps = MaintenanceCapabilities.For("Fat"); + public void GivenFat_WhenCapabilitiesAreAsked_ThenExtentDefragmentSortAndTheirModesAreOffered() { + var caps = TargetCapabilities.For("Fat"); Assert.Multiple(() => { Assert.That(caps.Verb(MaintenanceVerb.Defragment).Supported, Is.True); Assert.That(caps.Verb(MaintenanceVerb.Shrink).Supported, Is.True); Assert.That(caps.Verb(MaintenanceVerb.WipeEmpty).Supported, Is.True); Assert.That(caps.Verb(MaintenanceVerb.Scramble).Supported, Is.True); - Assert.That(caps.Verb(MaintenanceVerb.Optimize).Supported, Is.False, "a filesystem's layout operation is Defragment; Optimize would copy it through"); - foreach (var strategy in new[] { DefragStrategy.Consolidate, DefragStrategy.Defrag, DefragStrategy.Reorder, DefragStrategy.CarveHole }) + foreach (var strategy in new[] { DefragStrategy.Consolidate, DefragStrategy.Defrag, DefragStrategy.Reorder, DefragStrategy.CarveHole, DefragStrategy.SortEntries }) Assert.That(caps.Strategy(strategy).Supported, Is.True, strategy.ToString()); Assert.That(caps.PackAtStart.Supported && caps.PackAtEnd.Supported, Is.True); Assert.That(caps.Interleave.Supported, Is.True, "FAT's planner honours block interleave"); Assert.That(caps.MetadataZone.Supported, Is.True); - Assert.That(caps.LayoutProfile.Supported, Is.False, "FAT refuses layout templates, so the profile is not offered"); + Assert.That(caps.LayoutProfile.Supported, Is.False, "FAT does not declare layout templates, so the profile is not offered"); }); } - [Test] - public void GivenAnyFormat_WhenSortEntriesIsAsked_ThenItIsRefusedWithAReason() { - foreach (var format in new[] { "Fat", "Zip", "Ntfs", "ExFat" }) { - var sort = MaintenanceCapabilities.For(format).Strategy(DefragStrategy.SortEntries); - Assert.That(sort.Supported, Is.False, format); - Assert.That(sort.Reason, Does.Contain("sorting directory entries"), format); - } + [TestCase("Fat", true)] + [TestCase("ExFat", true)] + [TestCase("Ntfs", false)] + [TestCase("Zip", false)] + public void GivenAFormat_WhenSortEntriesIsAsked_ThenOnlyFatAndExFatOfferIt(string format, bool offered) { + var sort = TargetCapabilities.For(format).Strategy(DefragStrategy.SortEntries); + + Assert.That(sort.Supported, Is.EqualTo(offered)); + if (!offered) Assert.That(sort.Reason, Does.Contain("sorting directory entries")); } [Test] - public void GivenZip_WhenCapabilitiesAreAsked_ThenDefragmentIsNotOfferedButOptimizePurgeAndClearAre() { - var caps = MaintenanceCapabilities.For("Zip"); + public void GivenZip_WhenCapabilitiesAreAsked_ThenDefragmentIsNotOfferedButOptimizeIsWithItsMethods() { + var caps = TargetCapabilities.For("Zip"); + var profile = MaintenanceCapabilities.Describe("Zip")!; Assert.Multiple(() => { Assert.That(caps.Verb(MaintenanceVerb.Defragment).Supported, Is.False, "an archive has no free-space layout to defragment (#432)"); Assert.That(caps.Verb(MaintenanceVerb.Optimize).Supported, Is.True); - Assert.That(caps.OptimizeRoute, Is.EqualTo(OptimizeRoute.Reencode)); - Assert.That(caps.Verb(MaintenanceVerb.Purge).Supported, Is.True); - Assert.That(caps.Verb(MaintenanceVerb.WipeEmpty).Supported, Is.True); + Assert.That(caps.OptimizeMethods, Is.Not.Empty); + Assert.That(caps.OptimizeMethods.Contains(OptimizeMethod.Compress), Is.EqualTo(profile.Supports(MaintenanceCapability.Compress))); + Assert.That(caps.OptimizeMethods.Contains(OptimizeMethod.Repack), Is.EqualTo(profile.Supports(MaintenanceCapability.Repack))); Assert.That(caps.Verb(MaintenanceVerb.Scramble).Supported, Is.False); Assert.That(Enum.GetValues().Any(s => caps.Strategy(s).Supported), Is.False); }); @@ -76,12 +78,45 @@ public void GivenZip_WhenCapabilitiesAreAsked_ThenDefragmentIsNotOfferedButOptim [Test] public void GivenAPartitionedDisk_WhenCapabilitiesAreAsked_ThenNothingIsOffered() { - var caps = MaintenanceCapabilities.For("PartitionedDisk"); + var caps = TargetCapabilities.For("PartitionedDisk"); Assert.That(caps.AnySupported, Is.False, "a partitioned disk is read-only; each volume is maintained on its own"); Assert.That(caps.Verbs.Values.All(v => v.Reason.Length > 0), Is.True, "every refusal says why"); } + /// + /// The tab is a view of the registry's maintenance profile: for every registered format, each + /// ribbon answer must be exactly what says. + /// + [Test] + public void GivenEveryRegisteredFormat_WhenTheTabAsks_ThenItAgreesWithTheRegistryProfile() { + var mismatches = new System.Collections.Generic.List(); + foreach (var descriptor in FormatRegistry.All) { + var profile = MaintenanceCapabilities.Describe(descriptor); + var caps = TargetCapabilities.For(descriptor.Id); + void Check(string what, bool tab, bool registry) { + if (tab != registry) mismatches.Add($"{descriptor.Id} {what}: tab {tab}, registry {registry}"); + } + + Check("Defragment", caps.Verb(MaintenanceVerb.Defragment).Supported, + profile.Supports(MaintenanceCapability.DefragmentExtents) || profile.Supports(MaintenanceCapability.SortDirectoryEntries)); + Check("Optimize", caps.Verb(MaintenanceVerb.Optimize).Supported, + profile.Supports(MaintenanceCapability.Compress) || profile.Supports(MaintenanceCapability.Repack) || profile.Supports(MaintenanceCapability.Canonicalize)); + Check("Shrink", caps.Verb(MaintenanceVerb.Shrink).Supported, profile.Supports(MaintenanceCapability.Shrink)); + Check("Purge", caps.Verb(MaintenanceVerb.Purge).Supported, profile.Supports(MaintenanceCapability.Purge)); + Check("Clear", caps.Verb(MaintenanceVerb.WipeEmpty).Supported, profile.Supports(MaintenanceCapability.WipeUnused)); + Check("Compact", caps.Verb(MaintenanceVerb.Compact).Supported, profile.Supports(MaintenanceCapability.Compact)); + Check("Scramble", caps.Verb(MaintenanceVerb.Scramble).Supported, profile.Supports(MaintenanceCapability.Scramble)); + Check("Sort Entries", caps.Strategy(DefragStrategy.SortEntries).Supported, profile.Supports(MaintenanceCapability.SortDirectoryEntries)); + Check("Carve Hole", caps.Strategy(DefragStrategy.CarveHole).Supported, + profile.Supports(MaintenanceCapability.DefragmentExtents) && profile.Supports(DefragFeature.CarveHole)); + Check("Interleave", caps.Interleave.Supported, + profile.Supports(MaintenanceCapability.DefragmentExtents) && profile.Supports(DefragFeature.Interleave)); + } + + Assert.That(mismatches, Is.Empty); + } + // ── typed values ──────────────────────────────────────────────────────────────────────────── [TestCase("0", false)] @@ -258,6 +293,41 @@ public void GivenAFragmentedFat_WhenDefragmented_ThenSizeAndEveryFileAreUnchange }); } + [Test] + public void GivenAnUnsortedFat_WhenSortEntriesRuns_ThenTheRootIsInNameOrderAndSizeAndEveryFileAreUnchanged() { + var path = MaintenanceFixtures.UnsortedFat(this._root); + var size = new FileInfo(path).Length; + Assume.That(MaintenanceFixtures.RootOrder(path), Is.Not.EqualTo(MaintenanceFixtures.NameOrder(MaintenanceFixtures.RootOrder(path))), "the fixture starts unsorted"); + var presenter = new MaintenancePresenter(path, "Fat") { Verb = MaintenanceVerb.Defragment, Strategy = DefragStrategy.SortEntries, InterleaveText = "0" }; + + var outcome = presenter.RunAsync(_ => { }, _ => { }, CancellationToken.None).Result; + + var order = MaintenanceFixtures.RootOrder(path); + Assert.Multiple(() => { + Assert.That(outcome.Kind, Is.EqualTo(MaintenanceOutcomeKind.Succeeded), outcome.Summary); + Assert.That(order, Is.EqualTo(MaintenanceFixtures.NameOrder(order)), "the root is in name order"); + Assert.That(new FileInfo(path).Length, Is.EqualTo(size)); + var after = MaintenanceFixtures.ReadFatFiles(path); + foreach (var (name, data) in MaintenanceFixtures.UnsortedFatFiles) + Assert.That(after[name], Is.EqualTo(data), name); + Assert.That(presenter.CanCancel, Is.False, "a directory sort is one pass, not a move that can stop part-way"); + }); + } + + /// fsck.fat accepts the sorted volume. Needs WSL with dosfstools; skipped otherwise. + [Test, Category("ExternalFsInterop")] + public void GivenAnUnsortedFat_WhenSortEntriesRuns_ThenFsckFatAcceptsTheVolume() { + if (!FsInteropToolbox.WslAvailable || !FsInteropToolbox.WslHasTool("fsck.fat")) + Assert.Ignore("needs WSL with fsck.fat (dosfstools)"); + var path = MaintenanceFixtures.UnsortedFat(this._root); + var presenter = new MaintenancePresenter(path, "Fat") { Verb = MaintenanceVerb.Defragment, Strategy = DefragStrategy.SortEntries }; + Assume.That(presenter.RunAsync(_ => { }, _ => { }, CancellationToken.None).Result.Kind, Is.EqualTo(MaintenanceOutcomeKind.Succeeded)); + + var (stdOut, stdErr, exitCode) = FsInteropToolbox.RunWsl($"fsck.fat -n -V {FsInteropToolbox.WinToWsl(path)}"); + + Assert.That(exitCode, Is.EqualTo(0), $"fsck.fat rejected the sorted volume:\n{stdOut}\n{stdErr}"); + } + [Test] public void GivenAHoleLargerThanTheVolume_WhenCarved_ThenTheRunIsRefusedAndTheImageIsByteIdentical() { var path = MaintenanceFixtures.FragmentedFat(this._root); diff --git a/README.md b/README.md index 561917aae..087f4475c 100644 --- a/README.md +++ b/README.md @@ -361,8 +361,9 @@ selected. While it is selected the block map takes the whole client area — lin platter or a 3-D stack of platters, with an optional files panel and colour key — and the ribbon picks the operation (Defragment, Optimize, Shrink, Compact, Clear free space, Purge, Scramble), the defragmentation mode (Consolidate at the start or end, Defrag by filling holes, Re-order into ascending -blocks, Carve a hole of a chosen size and offset), the block interleave (1–256), metadata placement and -layout profile. Each item is enabled only where the format supports it and says why when it is not; +blocks, Carve a hole of a chosen size and offset, or Sort Entries — directories sorted by name in place, +on FAT and exFAT), the block interleave (1–256), metadata placement, layout profile and, for Optimize, +whether to compress, repack or canonicalize. Each item is enabled only where the format supports it and says why when it is not; a request the format refuses leaves the image byte-identical. Leaving the tab returns the browser exactly as it was. From c62f69195456eda002715b18bdc76e8a89b25671 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 2 Oct 2026 14:09:43 +0200 Subject: [PATCH 11/16] # the Defragment tab no longer takes a plain file for a container by its name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: with f19.bin selected inside an open FAT16 volume, the tab worked on f19.bin as a BIN/CUE image. It showed "Real archive layout — 0 regions, 12.6 KB wasted" and an empty files panel, and disabled nearly every operation. The open volume should have been the target. Root cause: an entry inside the open archive became the target by DetectByExtension on its name alone, ahead of the open volume. Fix: - An entry of the open archive is a candidate only when its content proves it a container. It is opened with ArchiveOperations.OpenEntry and identified with FormatDetector.DetectByContent on its head (up to 1 MiB when the entry stream cannot seek); the verdict is cached per archive, timestamp and entry. A file on disk is still judged by DetectCached. - A Target field on the tab lists "Open volume (disk.img)" and, when proven, "Selected: inner.img (Fat)". The open volume is the default; the list's context-menu verbs aim at the proven selection. - The status line names the target: "disk.img — FAT Filesystem Image · 16.0 MB". - Changing the selection on the tab never retargets. It only adds or removes the offer, a chosen selected target stays listed, and nothing is re-evaluated while an operation runs. The tab moves to another volume only when the open volume it worked on is no longer open. Tests: a plain .bin selected inside a FAT volume leaves the volume as the target with every FAT operation enabled. A real FAT image inside a ZIP is offered and becomes the target when picked. fake.img (plain bytes) and notes.txt are not offered. A selection change keeps the chosen target and the picked mode. --- .../ViewModels/MainViewModel.cs | 163 +++++++++++------- .../ViewModels/MaintenanceSession.cs | 2 +- .../ViewModels/MaintenanceTarget.cs | 30 ++++ Compression.NativeUI/Views/DefragmentView.cs | 22 ++- .../Views/MainForm.Defragment.cs | 133 ++++++++++++-- .../NativeUI/MainFormTests.Defragment.cs | 101 +++++++++++ .../NativeUI/MaintenanceFixtures.cs | 24 +++ 7 files changed, 394 insertions(+), 81 deletions(-) create mode 100644 Compression.NativeUI/ViewModels/MaintenanceTarget.cs diff --git a/Compression.NativeUI/ViewModels/MainViewModel.cs b/Compression.NativeUI/ViewModels/MainViewModel.cs index ce59f01f4..1ded0cc36 100644 --- a/Compression.NativeUI/ViewModels/MainViewModel.cs +++ b/Compression.NativeUI/ViewModels/MainViewModel.cs @@ -228,7 +228,7 @@ public MainViewModel() { WipeEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.WipeEmpty), _ => CanMaintain(MaintenanceVerb.WipeEmpty)); CompactEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.Compact), _ => CanMaintain(MaintenanceVerb.Compact)); ScrambleEntryCommand = new RelayCommand(_ => OpenMaintenance(MaintenanceVerb.Scramble), _ => CanMaintain(MaintenanceVerb.Scramble)); - MaintenanceCommand = new RelayCommand(_ => MaintenanceRequested?.Invoke(this, null), _ => HasMaintenanceTarget); + MaintenanceCommand = new RelayCommand(_ => MaintenanceRequested?.Invoke(this, new(null, PreferSelection: false)), _ => HasMaintenanceTarget); ReconfigureEntryCommand = new RelayCommand(_ => Reconfigure(), _ => CanReconfigure()); DeleteSelectedCommand = new RelayCommand(_ => DeleteSelectedEntries(), _ => CanDeleteSelected); CopyCommand = new RelayCommand(_ => PutSelectionOnClipboard(cut: false), _ => CanCopySelection()); @@ -816,20 +816,10 @@ async Task DeleteAndReportAsync() { } /// - /// Resolves which archive a maintenance verb should target, without - /// materialising anything (cheap enough for CanExecute). Two cases, in - /// priority order: - /// - /// A single selected non-directory entry whose extension routes to a - /// recognized archive/filesystem format — either a real on-disk file - /// (OS-browser) or an archive nested inside the currently-open archive. - /// is set to it. - /// Otherwise the currently-open archive itself (which is a temp file - /// when the user has descended into a nested archive — so the verbs remain - /// available "even when this is an archive within another one"). - /// - /// Returns false (verb disabled) when neither applies or the registry - /// is not yet warm. + /// Resolves which container a maintenance command acts on: a single selected file whose + /// content proves it a container (), else the + /// archive the shell is inside. A name alone never decides — a plain f19.bin inside a FAT + /// volume is not a BIN/CUE image, and the volume stays the target. /// private bool TryResolveMaintenanceTarget(out string formatId, out ArchiveEntryViewModel? archiveEntry) { formatId = ""; @@ -838,38 +828,103 @@ private bool TryResolveMaintenanceTarget(out string formatId, out ArchiveEntryVi // fires CommandManager.InvalidateRequerySuggested when registration ends. if (!Compression.Lib.FormatRegistration.IsReady) return false; - if (SelectedEntries.Count == 1) { - var e = SelectedEntries[0]; - if (!e.IsDirectory && !e.IsParentEntry) { - // A file on disk is judged by its content (cached, since this runs on every requery); an - // entry inside an archive has no bytes at hand, so its name decides. - var probeName = IsBrowsingOsFolder ? e.Path : e.Name; - if (!string.IsNullOrEmpty(probeName)) { - var f = IsBrowsingOsFolder && File.Exists(e.Path) - ? FormatDetector.DetectCached(e.Path) - : FormatDetector.DetectByExtension(probeName); - if (f != FormatDetector.Format.Unknown && !FormatDetector.IsStreamFormat(f)) { - // OS-browser candidate must actually exist on disk. - if (!IsBrowsingOsFolder || File.Exists(e.Path)) { - formatId = f.ToString(); - archiveEntry = e; - return true; - } - } - } - } + if (ProveSelectedContainer() is { } selected) { + (formatId, archiveEntry) = (selected.Format.ToString(), selected.Entry); + return true; } - // Fall back to the open archive itself — while the shell is in it; a folder on disk being - // browsed is not the archive left behind. - if (HasArchive && !IsBrowsingOsFolder && !string.IsNullOrEmpty(ArchivePath) && !string.IsNullOrEmpty(Format) - && Format != FormatDetector.Format.Unknown.ToString()) { - formatId = Format; + if (OpenVolumeFormat() is { } open) { + formatId = open; return true; } + return false; } + /// The open archive's format while the shell is inside it; a folder on disk being browsed is not the archive left behind. + private string? OpenVolumeFormat() + => HasArchive && !IsBrowsingOsFolder && !string.IsNullOrEmpty(ArchivePath) && !string.IsNullOrEmpty(Format) + && Format != FormatDetector.Format.Unknown.ToString() + ? Format + : null; + + // Content verdicts for entries of the open archive, so a requery does not reopen the entry. + private readonly Dictionary _entryFormats = new(StringComparer.Ordinal); + + /// How much of an entry is read to identify it when its stream cannot seek. + private const int EntryProbeBytes = 1 << 20; + + /// + /// The single selected file, when its content identifies it as a container format (not a bare + /// stream): on disk by , inside the open archive by + /// opening the entry and running on its head. + /// + private (FormatDetector.Format Format, ArchiveEntryViewModel Entry)? ProveSelectedContainer() { + if (SelectedEntries.Count != 1) return null; + var e = SelectedEntries[0]; + if (e.IsDirectory || e.IsParentEntry || string.IsNullOrEmpty(e.Path)) return null; + + FormatDetector.Format format; + if (IsBrowsingOsFolder) { + if (!File.Exists(e.Path)) return null; + format = FormatDetector.DetectCached(e.Path); + } else if (HasArchive && File.Exists(ArchivePath)) { + format = DetectEntryByContent(e); + } else { + return null; + } + + return format == FormatDetector.Format.Unknown || FormatDetector.IsStreamFormat(format) ? null : (format, e); + } + + private FormatDetector.Format DetectEntryByContent(ArchiveEntryViewModel entry) { + var key = $"{ArchivePath}|{File.GetLastWriteTimeUtc(ArchivePath).Ticks}|{entry.Path}"; + if (_entryFormats.TryGetValue(key, out var known)) return known; + + var format = FormatDetector.Format.Unknown; + try { + using var stream = ArchiveOperations.OpenEntry(ArchivePath, entry.Path, password: null); + if (stream.CanSeek) { + format = FormatDetector.DetectByContent(stream); + } else { + var head = new byte[EntryProbeBytes]; + var read = 0; + for (int n; read < head.Length && (n = stream.Read(head, read, head.Length - read)) > 0;) read += n; + using var prefix = new MemoryStream(head, 0, read, writable: false); + format = FormatDetector.DetectByContent(prefix); + } + } catch { + // An entry that cannot be opened or read is not a target; the open volume stays one. + } + + return _entryFormats[key] = format; + } + + /// The open volume as a maintenance target, when it supports any operation. + internal MaintenanceTarget? OpenVolumeMaintenanceTarget + => FormatRegistration.IsReady && OpenVolumeFormat() is { } format && TargetCapabilities.For(format).AnySupported + ? new(MaintenanceTargetKind.OpenVolume, $"{format}|{ArchivePath}", format, Path.GetFileName(ArchivePath), Entry: null) + : null; + + /// The selected file as a maintenance target, when its content proves it a container that supports any operation. + internal MaintenanceTarget? SelectedMaintenanceTarget { + get { + if (!FormatRegistration.IsReady || ProveSelectedContainer() is not var (format, entry)) return null; + var id = format.ToString(); + if (!TargetCapabilities.For(id).AnySupported) return null; + var key = $"{id}|{(IsBrowsingOsFolder ? entry.Path : ArchivePath + "::" + entry.Path)}"; + return new(MaintenanceTargetKind.Selected, key, id, entry.Name, entry); + } + } + + /// Every target the Defragment tab can offer now: the open volume first, then a proven selection. + internal IReadOnlyList MaintenanceTargets() { + var targets = new List(2); + if (OpenVolumeMaintenanceTarget is { } open) targets.Add(open); + if (SelectedMaintenanceTarget is { } selected && targets.All(t => t.Key != selected.Key)) targets.Add(selected); + return targets; + } + /// /// True when the resolved maintenance target supports . Asks the same /// capability adapter the Defragment tab enables its ribbon from, so a context-menu item is never @@ -879,35 +934,25 @@ private bool CanMaintain(MaintenanceVerb verb) => TryResolveMaintenanceTarget(out var formatId, out _) && TargetCapabilities.For(formatId).Verb(verb).Supported; /// Whether there is a maintenance target that supports at least one operation. - internal bool HasMaintenanceTarget - => TryResolveMaintenanceTarget(out var formatId, out _) && TargetCapabilities.For(formatId).AnySupported; - - /// - /// Identifies the resolved maintenance target — the selected archive file or entry, else the open - /// archive — so the view can tell whether a session still belongs to what is selected. Null when - /// there is none. - /// - internal string? MaintenanceTargetKey - => TryResolveMaintenanceTarget(out var formatId, out var entry) - ? $"{formatId}|{(entry is null ? ArchivePath : IsBrowsingOsFolder ? entry.Path : ArchivePath + "::" + entry.Path)}" - : null; + internal bool HasMaintenanceTarget => MaintenanceTargets().Count > 0; /// - /// Raised when a maintenance command asks for the Defragment tab, with the operation to preselect - /// (null for none). The view owns the tab, so it is the one that opens it. + /// Raised when a maintenance command asks for the Defragment tab: the operation to preselect (null + /// for none) and whether the command was aimed at the selection rather than the open volume. /// - public event EventHandler? MaintenanceRequested; + public event EventHandler? MaintenanceRequested; - private void OpenMaintenance(MaintenanceVerb verb) => MaintenanceRequested?.Invoke(this, verb); + // The list's context menu acts on what is selected; the tab's own entry defaults to the open volume. + private void OpenMaintenance(MaintenanceVerb verb) => MaintenanceRequested?.Invoke(this, new(verb, PreferSelection: true)); /// - /// Opens the resolved target for maintenance. A real file — on disk, or the open archive — is + /// Opens for maintenance. A real file — on disk, or the open archive — is /// maintained in place. An archive nested inside the open archive is extracted to a temporary file, /// maintained there, and written back into the host after each change when the host is /// ; the session deletes the copy when disposed. /// - internal MaintenanceSession? OpenMaintenanceSession() { - if (!TryResolveMaintenanceTarget(out var formatId, out var entry) || MaintenanceTargetKey is not { } key) return null; + internal MaintenanceSession? OpenMaintenanceSession(MaintenanceTarget target) { + var (key, formatId, entry) = (target.Key, target.FormatId, target.Entry); if (entry != null && IsBrowsingOsFolder) { if (string.IsNullOrEmpty(entry.Path) || !File.Exists(entry.Path)) return null; diff --git a/Compression.NativeUI/ViewModels/MaintenanceSession.cs b/Compression.NativeUI/ViewModels/MaintenanceSession.cs index 778e69b0a..ffdec6de1 100644 --- a/Compression.NativeUI/ViewModels/MaintenanceSession.cs +++ b/Compression.NativeUI/ViewModels/MaintenanceSession.cs @@ -14,7 +14,7 @@ internal sealed class MaintenanceSession( Action? cleanup) : IDisposable { private Action? _cleanup = cleanup; - /// Identifies the target, as does. + /// Identifies the target, as does. public string Key { get; } = key; /// The file the operations read and write. diff --git a/Compression.NativeUI/ViewModels/MaintenanceTarget.cs b/Compression.NativeUI/ViewModels/MaintenanceTarget.cs new file mode 100644 index 000000000..845fd87b7 --- /dev/null +++ b/Compression.NativeUI/ViewModels/MaintenanceTarget.cs @@ -0,0 +1,30 @@ +namespace Compression.NativeUI.ViewModels; + +/// What a maintenance target is to the shell: the volume it has open, or something selected. +internal enum MaintenanceTargetKind { + /// The archive or image the shell is inside. + OpenVolume, + + /// A selected file — on disk, or an entry of the open archive — whose content proved it a container. + Selected, +} + +/// +/// One thing the Defragment tab can work on, as the Target field lists it. +/// +/// Whether it is the open volume or a selection. +/// Identifies the target across requeries. +/// The format its content was detected as. +/// The file name the user knows it by. +/// The selected entry, for . +internal sealed record MaintenanceTarget(MaintenanceTargetKind Kind, string Key, string FormatId, string Name, ArchiveEntryViewModel? Entry) { + /// How the Target field shows it. + public string Label => this.Kind == MaintenanceTargetKind.OpenVolume + ? $"Open volume ({this.Name})" + : $"Selected: {this.Name} ({this.FormatId})"; +} + +/// A command asking for the Defragment tab. +/// The operation to preselect, or null for none. +/// Whether the command came from the selection (the list's context menu), so a proven selected container is the target rather than the open volume. +internal sealed record MaintenanceRequest(Maintenance.MaintenanceVerb? Verb, bool PreferSelection); diff --git a/Compression.NativeUI/Views/DefragmentView.cs b/Compression.NativeUI/Views/DefragmentView.cs index 7cc10f8d7..2065e4c91 100644 --- a/Compression.NativeUI/Views/DefragmentView.cs +++ b/Compression.NativeUI/Views/DefragmentView.cs @@ -64,10 +64,28 @@ DataGridViewColumn AddColumn(string header, Func selector, int /// The line above the map: how honest the picture is, or what the running operation is doing. public string StatusText { - get => this._status.Text; - set => this._status.Text = value; + get => this._statusText; + set { + this._statusText = value; + this.ShowStatusLine(); + } + } + + /// What is being maintained — "disk.img — FAT · 16.0 MB" — shown at the start of the status line. + public string TargetText { + get => this._targetText; + set { + this._targetText = value; + this.ShowStatusLine(); + } } + private string _statusText = "—"; + private string _targetText = ""; + + private void ShowStatusLine() + => this._status.Text = this._targetText.Length == 0 ? this._statusText : $"{this._targetText} │ {this._statusText}"; + /// Whether the files panel shows beside the map. public bool FilesPanelVisible { get => !this._split.Panel2Collapsed; diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index eb89abb62..d3254beed 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -54,6 +54,10 @@ internal sealed partial class MainForm { private readonly RibbonComboBox _optimizeMethodCombo = new("Method") { FieldWidth = 112 }; private readonly List _layoutProfileEntries = []; + private readonly RibbonComboBox _targetCombo = new("Target") { FieldWidth = 170 }; + private readonly List _offeredTargets = []; + private MaintenanceTarget? _sessionTarget; + private bool _pendingPreferSelection; private MaintenanceSession? _maintenanceSession; private MaintenancePresenter? _presenter; private MaintenanceVerb? _pendingVerb; @@ -73,20 +77,37 @@ internal sealed partial class MainForm { private void BuildDefragmentTab() { this._defragTab = new RibbonTab("Defragment"); - this._defragTab.Groups.AddRange(this.OperationGroup(), this.StrategyGroup(), this.OptionsGroup(), this.ViewGroup()); + this._defragTab.Groups.AddRange(this.TargetGroup(), this.OperationGroup(), this.StrategyGroup(), this.OptionsGroup(), this.ViewGroup()); this._ribbon.Tabs.Add(this._defragTab); this._diskTools.Add(this._defragTab); this._ribbon.ContextualTabGroups.Add(this._diskTools); this._ribbon.SelectedIndexChanged += (_, _) => this.OnRibbonTabChanged(); - this._model.MaintenanceRequested += (_, verb) => this.OpenDefragmentTab(verb); + this._model.MaintenanceRequested += (_, request) => this.OpenDefragmentTab(request.Verb, request.PreferSelection); this._model.LocationChanged += (_, _) => this.UpdateDiskTools(); CommandManager.RequerySuggested += (_, _) => this.UpdateDiskTools(); this.FormClosing += this.OnClosingWhileMaintaining; this.UpdateDiskTools(); } + /// + /// What the tab works on: the open volume by default, or a selected file whose content proved it a + /// container. Picking here is the only way the target changes while an image is being worked on; + /// selecting something else in the list only adds or removes the offer. + /// + private RibbonGroup TargetGroup() { + this.Field(this._targetCombo, "The volume or container the operations work on.", () => { + var index = this._targetCombo.SelectedIndex; + if (index >= 0 && index < this._offeredTargets.Count && this._offeredTargets[index].Key != this._sessionTarget?.Key) + this.OpenSession(this._offeredTargets[index]); + }); + + var group = new RibbonGroup("Target"); + group.Items.Add(this._targetCombo); + return group; + } + private RibbonGroup OperationGroup() { (MaintenanceVerb Verb, string Text, string Icon)[] verbs = [ (MaintenanceVerb.Defragment, "Defragment", IconKeys.Defragment), @@ -226,28 +247,88 @@ private string HoleAtText() => this._holePlacementCombo.SelectedIndex == 1 // ── the tab and the client area ───────────────────────────────────────────────────────────── - /// Shows the contextual tab while there is something to maintain, and follows the target. + /// Shows the contextual tab while there is something to maintain, and keeps the target offer current. private void UpdateDiskTools() { - var running = this._presenter?.IsRunning == true || this._maintenanceCancellation is not null; + var running = this.MaintenanceRunning; var visible = running || this._model.HasMaintenanceTarget || (this._defragActive && this._maintenanceSession is not null); if (!visible && this._ribbon.SelectedTab == this._defragTab) this._ribbon.SelectedIndex = 1; this._diskTools.Visible = visible; - // On the tab, a different target selected meanwhile — Back, Forward, a typed address — is taken - // up; a target that merely vanished (a refresh that dropped the selection) keeps the session. - if (this._defragActive && !running && this._model.MaintenanceTargetKey is { } key && key != this._maintenanceSession?.Key) - this.OpenSessionForTarget(); + // Only while idle: a running operation keeps its target and its offer whatever is selected. + if (this._defragActive && !running) this.RefreshTargets(); + } + + private bool MaintenanceRunning => this._presenter?.IsRunning == true || this._maintenanceCancellation is not null; + + /// + /// Re-evaluates what the Target field offers. The session keeps its target: a selection change only + /// adds or removes the offer of a selected container, and a selected target stays listed after the + /// selection moves off it. Only when the open volume the session works on is no longer open — Back, + /// a typed address — does the tab move to the volume that is open now. + /// + private void RefreshTargets() { + var targets = this._model.MaintenanceTargets().ToList(); + if (this._sessionTarget is { } current) { + var left = current.Kind == MaintenanceTargetKind.OpenVolume && targets.All(t => t.Key != current.Key) + && this._model.OpenVolumeMaintenanceTarget is not null; + if (left) { + this.OpenSession(DefaultTarget(targets, preferSelection: false)!); + return; + } + + if (targets.All(t => t.Key != current.Key)) targets.Insert(current.Kind == MaintenanceTargetKind.OpenVolume ? 0 : targets.Count, current); + } else if (DefaultTarget(targets, preferSelection: false) is { } first) { + this.OpenSession(first); + return; + } + + this.OfferTargets(targets); + } + + /// The open volume, unless the request came from the selection and a selected container is proven. + private static MaintenanceTarget? DefaultTarget(IReadOnlyList targets, bool preferSelection) + => (preferSelection ? targets.FirstOrDefault(t => t.Kind == MaintenanceTargetKind.Selected) : null) + ?? targets.FirstOrDefault(t => t.Kind == MaintenanceTargetKind.OpenVolume) + ?? targets.FirstOrDefault(); + + private void OfferTargets(List targets) { + this._syncingDefragRibbon = true; + try { + if (!this._offeredTargets.Select(t => t.Key).SequenceEqual(targets.Select(t => t.Key))) { + this._offeredTargets.Clear(); + this._offeredTargets.AddRange(targets); + this._targetCombo.Items.Clear(); + this._targetCombo.Items.AddRange(targets.Select(t => t.Label)); + } + + this._targetCombo.SelectedIndex = this._offeredTargets.FindIndex(t => t.Key == this._sessionTarget?.Key); + } finally { + this._syncingDefragRibbon = false; + } + + this.SyncDefragmentRibbon(); } - /// Selects the Defragment tab, preselecting when the target supports it. - internal void OpenDefragmentTab(MaintenanceVerb? verb) { + /// + /// Selects the Defragment tab, preselecting when the target supports it. + /// aims it at a proven selected container instead of the open volume. + /// + internal void OpenDefragmentTab(MaintenanceVerb? verb, bool preferSelection = false) { this.UpdateDiskTools(); if (!this._diskTools.Visible) return; this._pendingVerb = verb; - if (this._ribbon.SelectedTab == this._defragTab) this.ApplyPendingVerb(); - else this._ribbon.SelectedIndex = this._ribbon.Tabs.IndexOf(this._defragTab); + this._pendingPreferSelection = preferSelection; + if (this._ribbon.SelectedTab != this._defragTab) { + this._ribbon.SelectedIndex = this._ribbon.Tabs.IndexOf(this._defragTab); + return; + } + + // Already on the tab: a command aimed at the selection is an explicit choice, so it retargets. + if (preferSelection && !this.MaintenanceRunning && this._model.SelectedMaintenanceTarget is { } selected && selected.Key != this._sessionTarget?.Key) + this.OpenSession(selected); + this.ApplyPendingVerb(); } private void OnRibbonTabChanged() { @@ -257,9 +338,11 @@ private void OnRibbonTabChanged() { this._defragActive = onTab; if (onTab) { // A session still running an operation is kept, whatever is selected now: its file is in use. - var running = this._maintenanceCancellation is not null; - if (this._maintenanceSession is null || !running && this._model.MaintenanceTargetKey is { } key && key != this._maintenanceSession.Key) - this.OpenSessionForTarget(); + if (!this.MaintenanceRunning || this._maintenanceSession is null) { + var target = DefaultTarget(this._model.MaintenanceTargets(), this._pendingPreferSelection); + if (target is not null && target.Key != this._sessionTarget?.Key) this.OpenSession(target); + } + this._pendingPreferSelection = false; this.ApplyPendingVerb(); } else if (this._maintenanceCancellation is null) { this.CloseSession(); @@ -272,12 +355,13 @@ private void OnRibbonTabChanged() { this.UpdateDiskTools(); } - private void OpenSessionForTarget() { + private void OpenSession(MaintenanceTarget target) { this.CloseSession(); this._defragView.Clear(); - this._maintenanceSession = this._model.OpenMaintenanceSession(); + this._maintenanceSession = this._model.OpenMaintenanceSession(target); if (this._maintenanceSession is { } session) { + this._sessionTarget = target; this._presenter = new MaintenancePresenter(session.ImagePath, session.FormatId) { InterleaveText = this.InterleaveText(), HoleSizeText = this.HoleSizeText(), @@ -286,15 +370,22 @@ private void OpenSessionForTarget() { MetadataZone = this.SelectedMetadataZone(), }; this.OnLayoutProfileChanged(); - this._defragView.Log($"{session.DisplayName} — {session.FormatId}"); + var display = FormatRegistry.GetById(session.FormatId)?.DisplayName is { Length: > 0 } name ? name : session.FormatId; + this._defragView.TargetText = $"{session.DisplayName} — {display} · {MaintenanceInput.FormatSize(new FileInfo(session.ImagePath).Length)}"; + this._defragView.Log($"{session.DisplayName} — {display}"); this.AnalyzeTarget(); + } else { + this._defragView.TargetText = ""; } - this.SyncDefragmentRibbon(); + var targets = this._model.MaintenanceTargets().ToList(); + if (this._sessionTarget is { } current && targets.All(t => t.Key != current.Key)) targets.Add(current); + this.OfferTargets(targets); } private void CloseSession() { this._presenter = null; + this._sessionTarget = null; this._maintenanceSession?.Dispose(); this._maintenanceSession = null; } @@ -461,6 +552,10 @@ private void SyncDefragmentRibbon() { this._stopButton.Enabled = running && p?.CanCancel == true && !this._maintenanceCommitStarted && this._maintenanceCancellation is { IsCancellationRequested: false }; this._analyzeButton.Enabled = idle; + this._targetCombo.Enabled = !running && this._offeredTargets.Count > 1; + this._targetCombo.ToolTipText = this._sessionTarget is null + ? "Open or select an image to maintain." + : running ? "An operation is running on this target." : $"Working on {this._sessionTarget.Label}. {this._targetCombo.Tag}"; foreach (var (view, toggle) in this._viewToggles) toggle.Checked = this._defragView.Map.ViewMode == view; this._filesPanelToggle.Checked = this._defragView.FilesPanelVisible; this._legendToggle.Checked = this._defragView.LegendVisible; diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs index 4b8dedba8..87ad37022 100644 --- a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -88,6 +88,107 @@ public void GivenAPartitionedDisk_WhenOpened_ThenNoMaintenanceIsOffered() { })); } + // ── the target ────────────────────────────────────────────────────────────────────────────── + + private static void Select(MainViewModel model, string name) { + model.SelectedEntries.Clear(); + model.SelectedEntries.Add(model.Entries.First(e => e.Name == name)); + CommandManager.InvalidateRequerySuggested(); + } + + private static RibbonComboBox TargetCombo(MainForm shell) => Field(shell, "_targetCombo"); + + /// + /// A plain file named f19.bin inside a FAT volume is not a BIN/CUE image. Its name used to + /// make it the target — "0 regions", an empty files panel, nearly everything disabled — instead of + /// the volume the user had open. + /// + [Test] + public void GivenAPlainBinFileSelectedInsideAFatVolume_WhenTheTabOpens_ThenTheVolumeIsTheTargetWithEveryFatOperation() { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.FatWithPlainBinFiles(root)); + Select(model, "f19.bin"); + + Assert.That(model.SelectedMaintenanceTarget, Is.Null, "a .bin name with no container content proves nothing"); + + RibbonItemNamed(shell, "Maintenance").PerformClick(); + Settle(shell); + + Assert.Multiple(() => { + Assert.That(TargetCombo(shell).Items, Is.EqualTo(new[] { "Open volume (disk.img)" })); + Assert.That(TargetCombo(shell).SelectedIndex, Is.EqualTo(0)); + Assert.That(DefragView(shell).TargetText, Does.StartWith("disk.img — ").And.Contain("FAT")); + Assert.That(DefragView(shell).StatusText, Does.StartWith("Real on-disk layout")); + foreach (var operation in new[] { "Defragment", "Shrink", "Clear", "Scramble" }) + Assert.That(Toggle(shell, operation).Enabled, Is.True, operation); + Assert.That(Toggle(shell, "Sort Entries").Enabled, Is.True); + }); + })); + } + + [TestCase("fake.img")] + [TestCase("notes.txt")] + public void GivenAnEntryWhoseOnlyClaimIsItsName_WhenSelected_ThenItIsNotOfferedAsATarget(string name) { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.ZipWithNestedImage(root)); + Select(model, name); + + Assert.That(model.SelectedMaintenanceTarget, Is.Null); + Assert.That(model.MaintenanceTargets().Select(t => t.Kind), Is.EqualTo(new[] { MaintenanceTargetKind.OpenVolume })); + })); + } + + [Test] + public void GivenARealFatImageInsideAZip_WhenSelected_ThenTheTabOffersItAndPickingItMakesItTheTarget() { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.ZipWithNestedImage(root)); + Select(model, "inner.img"); + + Assert.That(model.SelectedMaintenanceTarget?.FormatId, Is.EqualTo("Fat"), "its content is a FAT boot sector"); + + RibbonItemNamed(shell, "Maintenance").PerformClick(); + Settle(shell); + Assert.Multiple(() => { + Assert.That(TargetCombo(shell).Items, Is.EqualTo(new[] { "Open volume (nested.zip)", "Selected: inner.img (Fat)" })); + Assert.That(TargetCombo(shell).SelectedIndex, Is.EqualTo(0), "the open volume is the default"); + Assert.That(Toggle(shell, "Defragment").Enabled, Is.False, "the ZIP has no extents to move"); + }); + + TargetCombo(shell).SelectedIndex = 1; + Settle(shell); + Assert.Multiple(() => { + Assert.That(DefragView(shell).TargetText, Does.StartWith("inner.img — ")); + Assert.That(Toggle(shell, "Defragment").Enabled, Is.True, "the nested FAT image moves extents"); + Assert.That(Toggle(shell, "Sort Entries").Enabled, Is.True); + }); + })); + } + + [Test] + public void GivenAChosenTarget_WhenTheSelectionChangesOnTheTab_ThenTheTargetStaysAsChosen() { + WithImages(root => WithShell(shell => { + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.ZipWithNestedImage(root)); + Select(model, "inner.img"); + model.DefragmentEntryCommand.Execute(null); // the context menu: aimed at the selection + Settle(shell); + Assume.That(DefragView(shell).TargetText, Does.StartWith("inner.img — ")); + Toggle(shell, "Sort Entries").PerformClick(); + + Select(model, "notes.txt"); + Settle(shell); + + Assert.Multiple(() => { + Assert.That(DefragView(shell).TargetText, Does.StartWith("inner.img — "), "a selection change never retargets silently"); + Assert.That(TargetCombo(shell).Items, Does.Contain("Selected: inner.img (Fat)"), "the chosen target stays listed"); + Assert.That(Toggle(shell, "Sort Entries").Checked, Is.True, "the configured operation is kept"); + }); + })); + } + // ── the client area ───────────────────────────────────────────────────────────────────────── [Test] diff --git a/Compression.Tests/NativeUI/MaintenanceFixtures.cs b/Compression.Tests/NativeUI/MaintenanceFixtures.cs index f6c069be7..b59315c47 100644 --- a/Compression.Tests/NativeUI/MaintenanceFixtures.cs +++ b/Compression.Tests/NativeUI/MaintenanceFixtures.cs @@ -67,6 +67,30 @@ public static List RootOrder(string path) { public static List NameOrder(IEnumerable names) => [.. names.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ThenBy(n => n, StringComparer.Ordinal)]; + /// A FAT floppy holding plain data files named *.bin — a name BIN/CUE claims, a content nothing does. + public static string FatWithPlainBinFiles(string folder, string name = "disk.img") { + var writer = new FileSystem.Fat.FatWriter(); + for (var i = 1; i <= 20; ++i) writer.AddFile($"f{i}.bin", Pattern(3_000 + i * 97, i)); + var path = Path.Combine(folder, name); + File.WriteAllBytes(path, writer.Build()); + return path; + } + + /// + /// A ZIP holding a real FAT image (inner.img), a file that only looks like one by its name + /// (fake.img, plain bytes) and a text file. + /// + public static string ZipWithNestedImage(string folder, string name = "nested.zip") { + var inner = new FileSystem.Fat.FatWriter(); + inner.AddFile("INNER.TXT", Pattern(5_000, 0x71)); + var path = Path.Combine(folder, name); + using var archive = ZipFile.Open(path, ZipArchiveMode.Create); + using (var stream = archive.CreateEntry("inner.img", CompressionLevel.NoCompression).Open()) stream.Write(inner.Build()); + using (var stream = archive.CreateEntry("fake.img").Open()) stream.Write(Pattern(40_000, 0x72)); + using (var writer = new StreamWriter(archive.CreateEntry("notes.txt").Open())) writer.Write("notes"); + return path; + } + public static string Zip(string folder, string name = "bundle.zip") { var path = Path.Combine(folder, name); using var archive = ZipFile.Open(path, ZipArchiveMode.Create); From 58a126defd5c0738132b4c1ff856628f03cd5af4 Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Fri, 2 Oct 2026 14:09:55 +0200 Subject: [PATCH 12/16] * the shell README describes the Defragment tab's Target field --- Compression.NativeUI/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Compression.NativeUI/README.md b/Compression.NativeUI/README.md index d860a5fe2..00f9aca64 100644 --- a/Compression.NativeUI/README.md +++ b/Compression.NativeUI/README.md @@ -58,6 +58,7 @@ backends, so Windows and Linux run the same code rather than two frontends drift | Group | Items | | --- | --- | + | Target | The open volume (default), or a selected file whose content — never its name — proves it a container: "Selected: inner.img (Fat)". Changing the selection never retargets; only picking here does | | Operation | Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Start; Stop while extents are being moved | | Defrag Mode | Consolidate (Pack at End as its variant), Defrag (fill holes), Re-order (ascending blocks), Carve Hole with its size, placement and offset fields — all extent moves — and Sort Entries, which sorts every directory by name in place and moves no data | | Options | Block interleave (spinner, 1–256), metadata placement, layout profile and Edit Profiles for extent moves; the seed for Scramble and the method for Optimize (Compress, Repack or Canonicalize, as the format offers) appear while their operation is picked | From 1a3bf392b5308397189bebee693fe76b92f0075b Mon Sep 17 00:00:00 2001 From: Hawkynt Date: Wed, 7 Oct 2026 15:32:26 +0200 Subject: [PATCH 13/16] # every Defragment group stays expanded at the default window width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: at 1180 px on GTK the new Target group, one combo over two empty rows, pushed Options and View into drop-downs, hiding interleave and the view modes the owner wants directly visible. Fix: - The target field is a small row of the Options group, under Profiles…, shown only when there is a choice to make. The header line always names the target. - Start leads the Operation group and Stop takes the free slot under Scramble, which saves a column. - "Edit Profiles" is now "Profiles…". A headless test asserts that no group collapses at the shell's default 1180 px width, both on a plain FAT volume and in the widest state (a nested target offered while Optimize shows its method). Widening a field makes it fail. --- Compression.NativeUI/README.md | 9 +++-- .../Views/MainForm.Defragment.cs | 36 ++++++++++--------- .../NativeUI/MainFormTests.Defragment.cs | 31 ++++++++++++++++ 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Compression.NativeUI/README.md b/Compression.NativeUI/README.md index 00f9aca64..e71eb0b30 100644 --- a/Compression.NativeUI/README.md +++ b/Compression.NativeUI/README.md @@ -58,12 +58,15 @@ backends, so Windows and Linux run the same code rather than two frontends drift | Group | Items | | --- | --- | - | Target | The open volume (default), or a selected file whose content — never its name — proves it a container: "Selected: inner.img (Fat)". Changing the selection never retargets; only picking here does | - | Operation | Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Start; Stop while extents are being moved | + | Operation | Start, then Defragment, Optimize, Shrink, Compact, Clear (wipe free space), Purge, Scramble as a radio group; Stop while extents are being moved | | Defrag Mode | Consolidate (Pack at End as its variant), Defrag (fill holes), Re-order (ascending blocks), Carve Hole with its size, placement and offset fields — all extent moves — and Sort Entries, which sorts every directory by name in place and moves no data | - | Options | Block interleave (spinner, 1–256), metadata placement, layout profile and Edit Profiles for extent moves; the seed for Scramble and the method for Optimize (Compress, Repack or Canonicalize, as the format offers) appear while their operation is picked | + | Options | Block interleave (spinner, 1–256), metadata placement, layout profile and Profiles… (the editor) for extent moves; the seed for Scramble and the method for Optimize (Compress, Repack or Canonicalize, as the format offers) appear while their operation is picked, and the Target choice appears only when there is one to make | | View | Blocks, Circle, 3D Stack; Files panel; Legend; Analyze (read the layout again) | + The line above the map names the target. It is the open volume, or a selected file whose content — + never its name — proves it a container ("Selected: inner.img (Fat)"); changing the selection never + retargets, only picking in the Target field does. + Every item asks one adapter (`Maintenance/TargetCapabilities.cs`) whether the target supports it, and is disabled with the reason in its tooltip when not. The adapter is a view of the registry's `MaintenanceCapabilities.Describe` profile — operations, honoured defrag features — so the tab, the diff --git a/Compression.NativeUI/Views/MainForm.Defragment.cs b/Compression.NativeUI/Views/MainForm.Defragment.cs index d3254beed..1c80559de 100644 --- a/Compression.NativeUI/Views/MainForm.Defragment.cs +++ b/Compression.NativeUI/Views/MainForm.Defragment.cs @@ -77,7 +77,7 @@ internal sealed partial class MainForm { private void BuildDefragmentTab() { this._defragTab = new RibbonTab("Defragment"); - this._defragTab.Groups.AddRange(this.TargetGroup(), this.OperationGroup(), this.StrategyGroup(), this.OptionsGroup(), this.ViewGroup()); + this._defragTab.Groups.AddRange(this.OperationGroup(), this.StrategyGroup(), this.OptionsGroup(), this.ViewGroup()); this._ribbon.Tabs.Add(this._defragTab); this._diskTools.Add(this._defragTab); @@ -92,22 +92,19 @@ private void BuildDefragmentTab() { } /// - /// What the tab works on: the open volume by default, or a selected file whose content proved it a - /// container. Picking here is the only way the target changes while an image is being worked on; - /// selecting something else in the list only adds or removes the offer. + /// Wires the target field: the open volume by default, or a selected file whose content proved it + /// a container. Picking here is the only way the target changes while an image is being worked on; + /// selecting something else in the list only adds or removes the offer. The field is a small row of + /// the Options group, shown only while there is a choice to make — the header line names the + /// target always — so it costs the ribbon no width when there is nothing to choose. /// - private RibbonGroup TargetGroup() { + private void WireTargetField() => this.Field(this._targetCombo, "The volume or container the operations work on.", () => { var index = this._targetCombo.SelectedIndex; if (index >= 0 && index < this._offeredTargets.Count && this._offeredTargets[index].Key != this._sessionTarget?.Key) this.OpenSession(this._offeredTargets[index]); }); - var group = new RibbonGroup("Target"); - group.Items.Add(this._targetCombo); - return group; - } - private RibbonGroup OperationGroup() { (MaintenanceVerb Verb, string Text, string Icon)[] verbs = [ (MaintenanceVerb.Defragment, "Defragment", IconKeys.Defragment), @@ -119,17 +116,21 @@ private RibbonGroup OperationGroup() { (MaintenanceVerb.Scramble, "Scramble", IconKeys.Defragment), ]; + this._startButton = this.RibbonAction("Start", IconKeys.Test, this.StartMaintenance, RibbonItemSize.Large, Keys.None, "Run the chosen operation"); + this._stopButton = this.RibbonAction("Stop", IconKeys.Remove, this.StopMaintenance, RibbonItemSize.Small, Keys.None, "Stop the running operation"); + this._analyzeButton = this.RibbonAction("Analyze", IconKeys.Refresh, this.AnalyzeTarget, RibbonItemSize.Small, Keys.None, "Read the image's layout again"); + + // Start leads; Stop fills the slot under Scramble, so the group needs no column of its own for it + // and the whole tab stays expanded at the default window width. var group = new RibbonGroup("Operation"); + group.Items.Add(this._startButton); foreach (var (verb, text, icon) in verbs) { var toggle = this.RibbonToggle(text, icon, RibbonItemSize.Small, Keys.None, text, @checked: false, on => this.ChooseVerb(verb, on)); this._verbToggles[verb] = toggle; group.Items.Add(toggle); } - this._startButton = this.RibbonAction("Start", IconKeys.Test, this.StartMaintenance, RibbonItemSize.Large, Keys.None, "Run the chosen operation"); - this._stopButton = this.RibbonAction("Stop", IconKeys.Remove, this.StopMaintenance, RibbonItemSize.Small, Keys.None, "Stop the running operation"); - this._analyzeButton = this.RibbonAction("Analyze", IconKeys.Refresh, this.AnalyzeTarget, RibbonItemSize.Small, Keys.None, "Read the image's layout again"); - group.Items.AddRange(this._startButton, this._stopButton); + group.Items.Add(this._stopButton); return group; } @@ -182,7 +183,7 @@ private RibbonGroup OptionsGroup() { () => this.WithPresenter(p => p.MetadataZone = this.SelectedMetadataZone())); this.Field(this._layoutProfileCombo, "A zone-based layout template: files go into named byte ranges with per-zone sort orders.", this.OnLayoutProfileChanged); - this._editProfilesButton = this.RibbonAction("Edit Profiles", IconKeys.Properties, () => { + this._editProfilesButton = this.RibbonAction("Profiles…", IconKeys.Properties, () => { new LayoutProfileEditor().ShowDialog(this); this.RefreshLayoutProfiles(); }, RibbonItemSize.Small, Keys.None, "Create, change or delete layout profiles"); @@ -192,9 +193,10 @@ private RibbonGroup OptionsGroup() { this.Field(this._optimizeMethodCombo, "How Optimize improves the container: compress (best compression, same format), repack (same entries, dead space dropped) or canonicalize (normal form). Only what the format offers is listed.", () => this.WithPresenter(p => p.OptimizeMethod = this.SelectedOptimizeMethod())); + this.WireTargetField(); var group = new RibbonGroup("Options"); group.Items.AddRange(this._interleaveSpinner, this._metadataZoneCombo, this._layoutProfileCombo, - this._editProfilesButton, this._seedSpinner, this._optimizeMethodCombo); + this._editProfilesButton, this._seedSpinner, this._optimizeMethodCombo, this._targetCombo); return group; } @@ -552,6 +554,8 @@ private void SyncDefragmentRibbon() { this._stopButton.Enabled = running && p?.CanCancel == true && !this._maintenanceCommitStarted && this._maintenanceCancellation is { IsCancellationRequested: false }; this._analyzeButton.Enabled = idle; + // Seed and Method never show together, so with Edit Profiles the target makes at most three rows. + this._targetCombo.Visible = this._offeredTargets.Count > 1; this._targetCombo.Enabled = !running && this._offeredTargets.Count > 1; this._targetCombo.ToolTipText = this._sessionTarget is null ? "Open or select an image to maintain." diff --git a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs index 87ad37022..c07b0cbad 100644 --- a/Compression.Tests/NativeUI/MainFormTests.Defragment.cs +++ b/Compression.Tests/NativeUI/MainFormTests.Defragment.cs @@ -118,6 +118,7 @@ public void GivenAPlainBinFileSelectedInsideAFatVolume_WhenTheTabOpens_ThenTheVo Assert.Multiple(() => { Assert.That(TargetCombo(shell).Items, Is.EqualTo(new[] { "Open volume (disk.img)" })); Assert.That(TargetCombo(shell).SelectedIndex, Is.EqualTo(0)); + Assert.That(TargetCombo(shell).Visible, Is.False, "with nothing to choose, the header line names the target and the field takes no room"); Assert.That(DefragView(shell).TargetText, Does.StartWith("disk.img — ").And.Contain("FAT")); Assert.That(DefragView(shell).StatusText, Does.StartWith("Real on-disk layout")); foreach (var operation in new[] { "Defragment", "Shrink", "Clear", "Scramble" }) @@ -154,6 +155,7 @@ public void GivenARealFatImageInsideAZip_WhenSelected_ThenTheTabOffersItAndPicki Assert.Multiple(() => { Assert.That(TargetCombo(shell).Items, Is.EqualTo(new[] { "Open volume (nested.zip)", "Selected: inner.img (Fat)" })); Assert.That(TargetCombo(shell).SelectedIndex, Is.EqualTo(0), "the open volume is the default"); + Assert.That(TargetCombo(shell).Visible, Is.True, "a choice to make shows the field"); Assert.That(Toggle(shell, "Defragment").Enabled, Is.False, "the ZIP has no extents to move"); }); @@ -189,6 +191,35 @@ public void GivenAChosenTarget_WhenTheSelectionChangesOnTheTab_ThenTheTargetStay })); } + /// + /// The owner wants interleave and the view modes as directly visible ribbon items, so no group of + /// the tab may fold into a drop-down at the default 1180 px width — neither in the plain case nor + /// in the widest one (a second target offered while Optimize shows its method). + /// + [Test] + public void GivenTheDefaultWindowWidth_WhenTheDefragmentTabIsShown_ThenNoGroupIsCollapsed() { + WithImages(root => WithShell(shell => { + var ribbon = Field(shell, "_ribbon"); + void AssertExpanded(string state) { + ribbon.PerformLayout(); + Assert.That(ribbon.Width, Is.EqualTo(1180), "the shell's default client width"); + var collapsed = DefragTab(shell).Groups.Where(g => g.IsCollapsed).Select(g => g.Text).ToList(); + Assert.That(collapsed, Is.Empty, state); + } + + OpenOnDefragmentTab(shell, MaintenanceFixtures.FragmentedFat(root)); + AssertExpanded("a FAT volume, one target"); + + var model = Field(shell, "_model"); + shell.OpenArchive(MaintenanceFixtures.ZipWithNestedImage(root)); + Select(model, "inner.img"); + Settle(shell); + Toggle(shell, "Optimize").PerformClick(); + Assume.That(TargetCombo(shell).Visible, Is.True); + AssertExpanded("a ZIP with a nested image offered and Optimize's method shown"); + })); + } + // ── the client area ───────────────────────────────────────────────────────────────────────── [Test] From 0405868889addcfd8fec05e77009c7615c35e9e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:09:26 +0000 Subject: [PATCH 14/16] * refresh the application screenshot --- screenshots/main-window.png | Bin 66879 -> 67598 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/screenshots/main-window.png b/screenshots/main-window.png index a08143529aa47bab2af870502feb042d22e1de30..b122a5b7006b79659cd5b19219b8459fbc7cf53d 100644 GIT binary patch literal 67598 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%0D03$U&rdIm8q}?R|JKq1iHSLN-6#3 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 KPyoC2@V@|SR1psV literal 66879 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 From e64330391e83a6faee2da4e4669ab636c1150b1a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:10:30 +0000 Subject: [PATCH 15/16] * refresh the archive browser screenshot --- docs/screenshots/archive-browser.png | Bin 66879 -> 67598 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshots/archive-browser.png b/docs/screenshots/archive-browser.png index a08143529aa47bab2af870502feb042d22e1de30..b122a5b7006b79659cd5b19219b8459fbc7cf53d 100644 GIT binary patch literal 67598 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%0D03$U&rdIm8q}?R|JKq1iHSLN-6#3 z-M8YWe1kV_64*G{GQ^k1i4yCg{;%wgdhuETm6b%CQ%ed1WsR{FyE_w^6Eo2x*&!h1 zeIUqGZ9rYelj}3Q!9w5wo=MA=4nVUM_`z3q-#Id(vGFc5J)B%=_jo<)zAsv%fSZR$ zw_~n;K>PWyQJR1KZ5>A`$l?$o$B}f-SG|cMsi36ne1F<`sj8R--(TH*nk@iOEHC!o zL^6ER8_8Mqq@VsV_tfhB4#WeVz2T05o_@AOcj8+^eWoO#OgJc!_mIxaKmIdQX($6~ zz^xw{>zl7OIBIPM<%sR=u%dqS<$FXgxQh-S%)7C=>%;VA#>WlsqehgKFS&cZdBh zo_RoZIb2`NG&swY-Tfa0$c);h$_Wtr&7Dnp;yv~{OV)oA=H~7fdA2OK z(hz})9bxbi7R0(o%*14MZEt>FV>>ta`?q1Ez*6BJ|5+J&B}T@uOg8d`-%XaU}xui zB|GbD)^QTBO4@Srum#*xP__1rDQRj-1$M-9xSA|)?~6Nsm(DabF-mjoDHDG5Kg)EL z-^XbfbBm%_v9U8N$jZLrG?I045)qHSh=eXj44_&j?#6OQN&ftQfD7;Dv&q2@`8Iz= zYi$V*hormIA)|jm7@&IWpcxt$nJFTGRAEzy`JBg*Xn}0w2G~hh$`|0S6(d>%#)M zSYo0K!izpGau{%Q`ttsyqM?!BC4+5PRr?n<3fJ@ZEvF#-Y#uFRL1itsek3L>X7CON z%p2P0p9HwaSQ`qwAOnAu>Egt`I9OV3@bEg@pG(e8{(!Lf--XDOe+!>xDX30#N;4nH z&&ydn;PH|E&fEWzhAm0v^>fE}W{dr$Q&p;qeF@0^kw@QDfH{Hs4#L9nc5rN{dUe~> zSVt@W+2tYPy&J{CF;^KBQ>EY^2~VC{3e+G$gkoF7BxLS&cR2&gbIr{)m~pWCwU18f z&-bxK@prdT(PHBh6B82>KDZA1FL!|DgAH7KXy~lLb-H;*QqkdhZx-Z|&KlF!k916n z0dpN49jz!$h4gF1L3#7m3q5zd7N|SpgTEI{k#bs+M>gl~9ddHAKF69#>SiyA_%z%- zD~{?-_JB{Q|H=gZ6M}u4JHQ?+=RcCiajajCa zo*fs=9?adZFGl@&|DFE;%tQ~bz`6Q+{-YoN!h*JjA6L!H=V@3>R&xQM%|vZHUBCnI zFeunSMnV#okN^gx6+JzHmgauVI>k+_&BH4Ce7efLsqo<77ai;Se70$ar|kYBU};`Y z+-48Ffxcm)rltiJ*oyrKqFTxT0*%8uv@54iw8*~swRVxnzrqgl6a@s9ZrXZY4%pM% z2UYxAj%#I`)fAZSLqL6Xg&Q0fQu2B82>1K<$w>~7#j4a7v{90&M#YLT!?lhL4UJoD zj~cYBNhPMH*x1mAVdtg@LC5Zkg!z9Q$2;?PV=nM5^N z;GJGR+Zw9ZRN{M#$C7m4L@LVXa`St6%Y3>_Zg<+OOuT2JP^;QzzhMX~bI^!Il#6SE zgBcN0!^v4{c(WhKx?N&pEh**41vSDwV#^y|pDJ=nzg{y3Wva>j@9pCD3Vl$OQbB3%NG*%ssBf`uT`THv;|L-jd(|~i zed)hRcMAh9zfg`WqzGE!w9p*qE(XC6^|@SXS@3^`yYIJ}&$kUsk~< zH16(w7VoyVI6g>iiHDE7Z+= zohwL3EW0*Go{5U7uH^NlLC8mafFlpsQ2wKU{3 z7?wT{4d4Od*@L4&b&GB>D5vECS;GDD>uLDcFX#rO?cY}aMfUmi#?BW4r?=JbBNK|w zfC>*Doo|~!>}PXtVC($t=iXRQopQc#c}T$Z>*sfN+^Sp>&0$P`5`ES^xc__xBCn(E zsM#U&1~>u2QGa|9T?^#ciKUV#T57pt3d)+$R~MbapkFnS|R>Tn_KT}j!`V8-9O@mWy*v)S|K7ToT896B%2ou2sXv|de9v){(2HQfZ$BHX^%;$V{-l*{iB zfMyN~9=oi?UJbEWqqhpUwc=JW?%lJPA74^<@q*~V0r);WB zh1x6jp^}^?x9MYQ(7Fi+niln zQm;02kAL$9?hG(FJsob|bUt-BRF-pOudF@VJG4=V$cqsPue)U|0gkUylZfRNSPi%oD zHig6sus=;*KSW-wf7~OmwX?ga@)KAQ0hPZqW z?&+bmS{^$()_Um0H#|7f5l$<_s}QQcxK|PPpM6kRftddpD57)!v*Y=|gMyMZRFFIT zpEdse@15dOQd=OTIOC{vJN;7`j z2oO(P3dsAAHgYO>B&vKw;bmLzB{a&C+w|K1AWFmy)h{fpOueNjhLH;w#s2%`w>UFo zBi|&Cdfuevf!pV?vr?Y@5e7&HTC9flmMI~7;7A|8TNB`Uyg~|KmIr3fg-C~5Xf+>8 z%Sr`do`|)e1t1AIcOllBdO!}6kP#6U5d#0+Bj9&@8!&z-<6Q8RULm))?d(To{PkT4 z*E{&^R*~#SZgW~f=3Av(n0&mAQ5;U&`#(4#&9OU+jn-9wp6{7R)bgxtu2QgQma)*e zUADYXEiN7^Qit2~5)1Jya@kEyP3)H2>tIks_xF>{w#H=Nj?A7P7Fk=6N**mWt+XF# zHyCAq^JZ#yZFtpkp(57BW#n@pckENpQ17{XFp#C(Dz&m~-(DzQ8zn|aE0Q^e|1MTW zpq5WK*&mSmqN3f!N==OIij4$XhOm_t3gJwB`U@o-oTeqpBkrnQUd3V0(k#G*D6un$ zM|0HI$97{<$EJdDlE2En*fGDcZ%aopBu<^(l^>6-o_T-mRD3PTNvER80F^vUO*@-vLj7A(D9ub z^<+R?BF0I7KfS5F9off0$HYV_Gg1FIH8Y| zpZm$->^zD8dLdl4pOR#2uPdI(F!Q>Tk3o6i!UCw=7?jedc)-mB*FA3*H8gC7aoaBi zGs6ZZDJA9nodWx>rn;D*sMGvh@u(4@oLq3dW_uI|G$vQKg311Y`H4i8)QjCV!hcpb zG}D%;37 zOzyFhV`hDjRYtN}%CkAE4rz4VpMJRN>S3VI{U#3Fk`x#;&-A-r3JB|C zoV4XPPp&h}Hc&<5jB|`I+QMR%8FpB#fpvkO%>m06OuBv(FXO7v|^wW!rvXmKo;q<*Py&j*#eGsUC zb=uL>gAh=)(vIC`@&flOgK$|I)vMhL7FVyjl`42lb{3=cKd~bWfIJrb#2-KCf1`A}r7k%Nu*ORvu(8Sm?c&j&U2i<%x2lY+lq(oqDdYub$X`k4Vv z+Txq0E^3O&`WDCQcUC4Ud8ByayG{3btS}&ed52!p8j+oxzjeoZjd?mR<@kFV{vk{A z{iTW9?o~LbZPLC=3z|$>)d$&F6JW9~g5vzDWUrVf-OzBK;%C12fs8N5`v_-F&GiG& z8~0fm+`fGadb%L)P*e;ccrc7>1gwRgDu4XEa4+{6O|hv!wdna_O_GWGw zbP1lj&>)q~w4SJN%8Yg4qFOoMDxA{zZaga}{{*qv{j(IqPW$M5cxt%ge8)A?Y;D^7 z_4h!$%RhY#sAx#HZeA+}X{f?i53ut09VUG5n$^oIOb@?}#$E6in`ec>!RB=O{yqsA zBj?%n!A;PBM-CUKuk?587S!MTsZ#!<@~orwS+40Nm&vVQn(KE6D(%lM`m~(1mCXi4 zmP<*2^~kSiWw@u|TkI6$(8R&{B@g?y;!^Gt782Qqs0jX-l&ZOTIXO=!XVWi#^scMh z1evvvAmXa4D>q?kK~6?k;ZRXw+t3!~jt$!)PgH8IslMG|<7%DlSy@Rz zG6|CDeZ-ZU=CpeKPqHHWjGO}g*citseu(rnJFEbci#`A-$QFw&*w$AOpy4%CjLJh zf}iFqo_9a^&lF5VBojq(N!P^r^`5r|sTiP_@?&zueHvH#;_X?J84$j|fg3GmDTm1w zm>qO$I7hyNeX3O{1=4h;h1V!mPdq#5Xi2YN=_0&rX`(oO5 zGnMRBcOV;F8d%ew--eXp!+0w_Q!c7pYi>{_K6u!3w#n^jcqE$2Ssgo zEJkfh@hu?1+69lV*~sTwq4URI=%4+-N$?W>dU#{|>Qz0xJlQ?t!Gc8d z?5g+%v6_-gze@SyK4=V&Egv9@k34hUY~|cu8CeRZiC{OSWP6^+v#08Q=K`vpDEU@x zAnO6Y5)OsLry2CYnMP$)jHEMrHMb|V&m~^7Iyn3Dop3nKtgBx>+Nph~8=#Cqg<^C1 zW*|reW!Fl&38B{mu8QZ{b#Jd+k7;<%!|fBrgoS-rRFY8FnbY)LoyUG}URAFR0#lh% z2E9dx>YVlU>QfFpTr_ervd(E!U^X(manA=>f}Paio+P(BXyIXVH*QT)9V-)1)vPI4 z;EBAxv(%TaT`9~yoyU6<%!)#R4#(TJt#wTh*6*2&rSwZN#vlzW?+E5=x$;-#|H8Z? zP-&%l_^XPard^zzdyJSus}}4)Q;=2NcAbjwi7Tg5&{CjR3iAZz{r%Xc7#gliGWP?= zuA`h9jf9CdN|?SQm;`o~AEzudmU_E+Z{E9qKbmc8qRgC;lf!9qe`3if{3M#wTB*ux zev->vTX-&~aDQWiqY6|Wj{Sw^q!uXwe77vEB59Q#qAJ5GYE{bz4a_iXtiDBB29y5n zO-|LgD+`;PXLKh0blwCL@?`sL(pPWm!Q|mYN(O0c87o zP7s}ai+_KNt5vIxXeW~krEbwQTW(GcusyIx!7VwPR0nIJNoGcHdFjI9VhNSfT@3tO zv$nE8gMI}C#fZZtdC;DZe{klo*(YNqO$O@9O_e$50>8m~G2# za?`eyX-Rr*u2%?nJRV4b||MB6*I~+!dQ01 zS+O@0wU)GF0D2SSiAqwt`ET)!S@}8>>lNqRE$h>ol=;D)M&RBY&rK(bt#KaPy|4!& zj9-DvIwQf`rXPLcUwp^0;#$Aft+29sg#X3oo%oBr}> z{(h_MHaT|nX86G^Z*rCATD?Dco$N1|m+FH71@twUDy4a;UqENS+4=1QOM%Hpmu{r( z`C(oqrK8?b^Qql$|AMiSljHvH09s_i|3RndcKlN$kwg#CB;ZJ^_{Z_MU1g>4a_s@ZZB=`*vM3Ukrgy+ZNcizp154y2;zn)RaKtrcgT*!3&#-@DChTOi?8g` zPCws=rP`89@1mEIMX)02)Q=O8r!!Y@->Y5`Cw4h?vfq{S!ze1sH8rz1KGpH`m`3L= z+d1g)R1fK`;|-w8{^Ucti*$q7B(u9%;i$S% z={%opf(lU?3yYdk{q$XKxT>I;l4whivae`yuA2`qGezCA>iWH49uRF57@w6c`!`8S zODCv4Bz)=9l3AIR?8FoDmNQ(cvclEGheF?OGzwLsVtfdE1e(!%XH;eq7mNqpaM=`m1zdf_B^Xm&a z{<)y+>_k`F(vN@3!E7P(Awo#E;-v3O<3+EpebnQq=21#EGxc3RKX|E;{{G@*6^?tW z+e6fhS;~^);wkcE#!y?tvj-ySdGEw6}(QR&E|xi1=kPQvZ2jz(lXDYrj~ z3b}*DqZE6I$n( zyCFN(XMlCRulj1YSKu)UYBLao^OD+%D9=+N49M~IkNw-dyZkRim z8KhgbGuh}+D}lRdyx6gLd63<7RKAH+2GClH3i5_yg{(wGMBW_}&hzfuVuz7R6ws8b zcs<!lN@bTyz{#R3wn1nys&QmG2R(|F-Ez&Mr;tGRLV3Ol`BREwSio-++M8)Ebl9 zb(JbtDjX~)JHLd;JApx6;8PA{Vq}j4;ZpqoGL-ISv!yH*dUt&Kuhy(apa zOir!3`r;SqjG+c%Wiqlk@Ts~(nMqBVlFqQZ&T^GfFYBPlX@>w`spt+Cvs;_?-r4{u zWNNaaXYudeH5X>P=X<@_+cnt+3mPhL`c*quJhs6%{$p2o;@|p*$UmlG_je$ri-aA z$@-s4w$+N~69nc>CN*eLiecKv0ltDe0!bLy%GnL~FZ5QDiw>SF4iPnBdYr+EyB2Jf zq`OY%N3*lQa_{!5mfHDAHU60DXfKVU;|gU70Z^?e2;scE7Bgc@YswF(=I}>fI({+ebNQG|ILHsduXXn!6aZ-d3mJDEojwI2KJ@-+!l}eZHfj`< z3*u{oLq+3x8o@_FRAA|dWGRZKzde@+?x`2tWW+?rL3b|1s7uL6C{!={@a@K(a=59H z!D~Fc{C_5wf8l4p(p(`8bZMYuV?sS||LK+-nMUZ|i`gJWuu)@CvQ`|=S&O^Qoru#HxS1%O)m=nYDn%?Sy-r~gNXp*~L4fnj%GOs^-b8%8m z>`xN!zlp*!@Bpq|`D4XP_2Y4k-&dL;byMc6_1rI^HGfWUxpYkaSqQNa>KYtH8nGod zlZ<&UIN%Y<`HSlYhsZiux3~8w?29wK0#jaYG81lTP$S*!Ijf|p4Nwmy%fajP5T|;= z%v(kH#3T+Vmm&o-M}=<;(wm8(%5|Oq=Xgtp?FS0oqiFW15|7xDum~ner%cxt^8XSn zSNw<$Yn%8L5k)l<8OxliyrP7_6T%rMo3bjmxcJ@qqDY1fzXFJHgti;}NA&VOl{!0BWA@LLR8xJz&BDyX_;wXQl_uGI592Yn*Sx zK|4*g2F!%BaQY)5>Fq>wo%R#Qso3a=Nc+81mKM28Z*Onr!#S6cflAL?x96zL&~KQg z`Pn5G1k@#AvN=X?u#lG<9)!;?&qe24{>EOQ`!pS{%WW{ zMiiQKBR2d=Jd2CWk?iii2)uH5v@%p({idy-fdRlloQt;fbiJuKn>RMk83PZWFrTZ; z($j?kmzI{$xxwF%iV){f@gmoG`T*40P>)bsYo-MY7xyb=+-TJ9OtFc)jvAji092Xa z^CfV87VdaLmbx!4%GH#ACv=$H(>N#$>DPdshlIz>KU*Ud_QcZSGgTxd(V~50&K)rT z!i~L=sQhS~ehMG%?vFG3q&wTT&dE2ie~jkoX~O zb1yy!UUy8YOHIZ>neOKsQewJPWF;!v`-3YLjTW>Z^m^q=UapU&NT$&$9|rg%fy(w2CG6kDazskR_ApJE(*O|TesI<*tt z?Yd8*7>X@pTlk43XGo(qXoc0y6PqW#D*_j9>}N-M|J2ez#_87jmr(P$#H2)elYWa$ z1oEwGmo?7Sp-_7(C2#NA{O8I|K)hmi|F%{oIzTgbvxe1R|yIYJ!^pac`;BQ|% zh{C&|?{==;Aj7)|wMBUB)6=Kq>yjyasuHHYq2JG`;U&@oR;I z;$aBtAHVwF-i{=$28?9I5oS$I&4<5EJS=sDSgfXgbA8U8OGtk#`Ka8QOQ%mRpPV$0 z8yRR2ljoWm(x`ts4!K!Vv+car@>ENnVtHUK_{R4= zA6(d;xMSbWMODHbLx_q5hhLUbB`!5qBCFlAlp03YoLh^^7(Vt?n?yq+A6iN;D$j@+53B1UT%#)sphCvUr|kM$(F8boo6 zz{7oB%u9*kf$A3&RAqa#t8=l#dne1sOtD&q+ z*iSdl*3>*I)@q^1WYC^*kHNf16U{MR;SxReIyr zc@0ugy47#hy+#t99`D#kiUTRsa%}XV4mNA9lcmZQ9T0Kc7-p%4{+&t*%mE!Ijx~Ef zEkSKRw51HW(snJMGQDZrz6%_WB4y_BI~Nz-mOC5|RqXdlTce1zX5&CL{{Fr58s@!w z7;NO zyR{dZUutU$QHENs#j~BAMZyL??Zk`5@=zi2fB8P?7xKfQ_=boPA- zaf*%Y=a=G>l7fPRtLMJD9NobO=F4$^vt4O;cvhC^?ZrC;>_vcEfU{XsQ&U(_5Jasw z*wE#I`Y6`v$jS&XogaA-*@{)iWyRL@+U36~77o1=SED=Rbmofx0#O`g*jidz+@o0H z+i-zJ08pS0XY}%AH3I{4zFRmXgE&)qRhp`G~Svznw*vwatfh3%i zm>gdCRHfpz3sgobE?$(j*vOkp*r)~)Dmphtt2q{%LtStRSuKeOc_t+h5kS|5n`@>Z zHFW1A^5D}~R1`K`E)u~X#zi4a0Ngpwb$jkt^^T#46S*OF4VctKKIYPI-qiHOxhDB=2ES!=iv zlGGr+CVKW$NIHN)>86S21uteoHmj+GZDe(ICnt5|Rp%Bnfg~|wPqF&uTQV9SIIJIM zudkk52ZuDo(V9K|nXsRh%lY7gZ31{C5llPI-YC25WtPMUkZ(q!KDu`UqTg!Wh3Bvq zi)BY4e0ha}mvqgIm3La(T9hh|?{u*QXDh16%iC;sT-HxZyY7P3Xy$UmzBK#digU;8A;<@fMk;a>6Nw{5*PxSBuWe$bMu3h(WE|E8 zU<4>#Q%7;gJfc*{wQJi9Oy6A{3VanF)^QDD{M+J~YXOlmO(4Ei5dV60+ZR{MCe?EGS ze3YGM&o{v9QNQwHDPK}j5a~(z2o)8Tm(8AcTmNEzV!ltd$(%P{fP98WFuPKrVUv1( zA}&K8p2`F>Q=)<_bkvk>U%zREHrN-`OLW7C^}!AF^4%3ug!~9I_!2}OOjH#H&hXya zzV#`o*Va9bD^P~d2Ny7V@4-uFGU1#xAy4NV1#Z{0&!0a;s@P~g4n&olx3Ie@)gw{+ zk<(^oAX%;hEC!5RT)l)GUT^|Y;W>gWH{rXAr-g#QpC_onZ?om0`xK* z3KxNa%ZG$TYlcX4y-KKA*BNVn2Ul2B;M`gz?hM@&sQOVJU236M;wA$y7?3aQ>{Sx` zD>Q$+CnVxePDp_9Pcp-3DB)x8_ztYSBcSf{b--lxXtcXie4ta@I+WI6xOE8ay{xn{ zIPxawJ6f(C0!0&wg?G7ui3W~B3?YH2Z@PL~L*312YaAEu&;I<0#tpaaOBmuH2&2yK z8$>}#cDr<|cLjXLmJ7XiyL2_XV!Cws&)D8>XHP>MIMqlh9}?>th=Oqe1T8WORBE4aNPSRZbgszIX`@4cN;GZ({Duk3^%!rKjgR4~Ta0$e`dX z@DEs{h%N+Y%8Md>sS>!*xsFyNfZiY$_H=x>op7G_f(+XHTCvXA>1TSSkwGSAmVQ1Q zfJ&v{+`7iY#l&raS$dbMOSG>yApK3w7Q~E8ze@ zhc7NlTJP-aq;l(jRLmu=?&b3f2}npuA-nOuDk^Fw=Kehl*dR|#DusWTmWDHAj8xgf zpc^-Vt)KDyUI|HcfNSVYJ+CjmBnCOJpkT&ldVq=lD0*EbPBzonn5~>|JUeWO)1Sdd zhpj-zWVkC?%qxAvbR`L-i!CT?6FMWqe1|8&x*cS;AC)>lCucexWOVr>iq6xSB;knt z<5?kkdf=xNDCBS3RFsc1h6wl1rpq};v+Xn=m`}O2-8Na%qNw)%+_<;;inEMtqO^qW za`_u(!k5jKmtK4jMxvnC`Pm8tPhjZQx5yce6m)$X_fAc*IUGGuDmFpqW-=62RdwRy zyB0WI-_Rhj1K0Sx#o@_Faj&#YOBN{$Ns;1C{TF35y2zKxkg3rr&YEjLmmge!uw$k; z>Ie(pl5wX(pvJZRn3< z`gr@~XghR(i>oNpsCl*J@mlpu`-a8Uw@df4vQ}4Dlu}3ToE$&6vV~(P$o*!jA#((#T}d20=p5)V&@x)d z)eoESLd)z?ONY-J*#HsT{ z_?$VCZ=M_l^!6IDS`Q5mzS@_O)YE&y$#L3mq=v46{wvOV!fe}qeDxxYDxg*7W@eN( zD{6dvit`G{ifR1K&Sb{$j4L#CBH5W(mKSdy_-IIVs~c%%sFbb$-vw48HDvGX+_GKIaGRf2L<;XW&CkIc6@ z_?)q9n2(X9!fFi&hgkgKc z>u$I)n4?{3Kf%=wLIWQ7r*Y|g3Jt8*Sf6UNk3wA)E=1~xxknTfICrs_Tk46QJsIl! z`r6|otADEd*GtlLT2lz%aq7`(&)&QO&MgBuqm%;dopffHbdi;%ZJArmaZe_xC`{ki(wc*gXZ#}P^0t%L7=pItt84y7|0rP#4S>nVPYoRnj z!g0{IhFnLd@#<=hJ9kiyS?rdUzMm&93wz6Ow*IVb#e{8a?0^-*2n@+Mn8oHDzy|Qy1kMP%%Ulf;L&;E8MsvAw4md^jmECF@- zj1XXv=Vhz&NA;V(#@GJa+gBd^kwK9MLD;+NR?NuvM-B#Ab)rAdug|?xLl};6YQ&iU=`<(@CwCkp6I!sSgx4-i6nA*)NkPr${8G4kAFmQV15=ag>+2zw zNfwTHy`@CK{XJ!((Fd2q@O8D5S4KHIg9)?s23lWW_T{Kg%~Fz*MazQ=XUDuivW0jj zsFVuuf(YM&g6+1EV$Pd3s?zd=(`K_yZCa?66S>ace41MaYO#8XQ}o0pGem>jkT z*^llBpH6r(;IWRCt%Qe%m+Ot}j$gi3xYTVFS^dsK@6kz4YXAIU?jIQ`VS=K_rFUUpPD;g` zm+J(Ahw>9LltZG`Lc)(G@|67+{BfqGa-Z|`4aRgt>Q`jj+N};&3}v`390&!&VCC@= zGXw|_y0tJY{>HU9cucxWguU)9#`#xnzTL3Rt6P+Zi81N8uif`YH!q|MI* zHNdz2?#)v3JqRf9hKqUDFQyR_(b57fv}QD-3?^ZLnfJ%o(H>ZXq!p_{glHshqbols zKKJcBopW<5D|4wj{Z`9Qa(W6(u}^FtxB~x3S){C*=YNy3r1a*FmKQ9=)x%Hu15HrEe8Yi=YjzN6xV0=lU&R**?61-EllRYkH|iNDFER^2u1%On2S z7GyRkZEfK=HyNOZSbS=ZVX@y|Cj2eUN?tJ99Q8mZR-%|6>k%2 zcJEJQkCmA2eXE72Tit;UWrEoZ_S4yOR2eZ0D@zR25~2|5;vxEY#{hmi;bRWMAK!?o z4Wfc?Z|@)xK47~*?;joxuxY6E1p)vaJ}8@E3qe`WT)R~9(e7ejq7?u6b`3~rypyf3 zUDe(+YK5N(qF<|PGdPWgWJR(Dr(@t??md6nIkH3^p2>@(koWeR@0WO3O9~1KnyZ8W zuKs6bRGNN(Ie)}*VHYeT(?8o5ZE=|li$FsR3A=KkgF&4FDU_t} zPn;r9Ra7EDr2QE`4JtUCWmV>H1*;9~&A8pS;hJ*`MimWv>IBu(ek&9%noB8uFPy8{ zkM`s;hK3s&n!0j}M&cW%KN)Y?RKPgZTPXW1_$8wvIKYjngtlI7g6&pUJYN_#j?|rn z#d&c?5KqBMs^;_Y?L~(>i_%yS?Zo*lFXIvu%hrc!@Oi&gda6ctZ*zKDE(`oND`mG0 zWQnl7D6w%F>{CY`k-L^y_1ORL)B3o#7GG6)U*JCidvd~il|qg68!!{@aGR=}k~2!} z3>^gfMItzi#zPEm=igrAF>bE=1Z-=-oe?91!|8>58q1d)KQw6qGiYb8Nh&*nlhk++ zP(`Xmn@cG+4r|`q@gAFz9zwJ^6hY|R3Cg4w=)NOzB+a$kj}^r58t2aJS*f$6KB7Fp zeb}Arc;vk@0uVM6g6L0DXVz=|A}~{y+g2;sW|&Jd0fwe9h>S}6noLyxR);7lx~iE2H4WVVjhn4GtdoJZ?S@7rusn*5Ciw6zCoc!M`AEo)<+@cA zfrs8W?D}giAHW(kR%Vf9G5CN%ue;cK&Fr9PJ3ifHZG0mCkJRFBvV{ZmXf6o6T$3y5G>sT(c)b9w0VN%k$&AxP>V|*fu3-nJ7-M2QX#dxcnqy56ljOM7p34&ZL zNKQpH+t@f!wqo$c@Vzcwo-PQ|J3^};PS$$>Hw9WALQ!&XG06sDAW(~fegdu#Vr(Qr zIDAVJPUaI8fFC19rFu`r!MTG8vU57&!NN=`1nFgJAV zIp%pXKbbRB8yR`VV!CN$V*?HDPcEm!fzB{V3!R(fl*1T6S+sLBA>oA}^vZ3+$0;(L zc&6F=00)xidN8ybWtqs%a4z9<=y&-u{H#EP6#D2vQPJUYZe~w)cg&3^fU6Vs_P0WDX8L9-hA*!(CI}? zNLS;Ncj5elKvezsEkNS@$%WscFZx;VgFTG@csa$M^DN;xfV#>qF22yy(@UJC!@m9P zSJnS9;)0igsIgO2OpJ1D+H;{O!jBQZ+KX zbRSuY@Bh+U_!ve%ecoG;?R1`!;^Boaa@DoIX+Z%jB0?&O5>Dm~Mo2^a}>kkcF z>_{{8qZ7F=H@AFn6Dp%H254Vn!4K-v;(>Ei%DvfQ(Z(t)a>ErV^YfARyHzMRzz$-p zr^l={bp@u=-{t;W#B89c~bN$i4o}-9J0Nr<9Yn66)$u z1H76=jiIeAY2}a5p9cHlhY{|G+;O^DYQ_9bW&)JYr)He^ z?H(LzzPoM#K`}(J+95YK1wY?N6QsnBg-jbNNrWU9IDZJf_kP{JU zY;K3TUUhZGV*yAi?|pA9=123qUt5xW7_-#uAx5B3RdwgP9hdvBzJ|ciW4$m%y-0|@ zp>%ZSxICXW+X8zff8X|QDNz1Nsc~SOotdeOv}P2iW%pd!K*_UspGc;zl45MrvE0`i z-B7*og2Bdf?R)r7zsEW@B1W z{U+=VWFUbONRUUmRAW1!1eokk-_IxOPGN`%2!yV%M5j5=ji2xpT}(;ODd-I>>G}qZYp1b#s>!UY*e3qdp^fMs#;%(t;tULbM$#+le z3*>IZ6sPmO@p8~rlg&`|4-F(%kLGo^F0MVQI_G-`DabqQ?<05ZyL6!r{Bc3dhlC^nlzMvy=E2PUoP{zlOz1qbotf|E^{6W( zq^F`HVX!(L?1IjUN0Z_7>uT#c6mIvCy!*gtrx?h{FET$*tr5jNX62lw<)wDi%*?YQ zZ`YTxdJ5_2Ay$DWAXWB_h~P%D-834ilsQCXEf%4+YF&n#%SoEs>mR-l{n08r7QgX6TJ`9W9E>bCH@6WWg7_dGa zEgk3Ji2ImjrxH$U(BOZxXSXx23Nt`U?ay3Bjt};XZHo&EjwdpP;F!@wvYJCK3I)QK zrQbGX(&OVbyiaW>cP(j%cxoY&cv<>psDViMr#p za`jZH{Sia9!2r&oJI#~3822!Ojwqho?KeE{@37DFzKA*N!Bb*TDL1VhnPVVKfOrWs zwsD%vadUwTC@5uREVHsDzfomK+k&0AiYNiA=|6=Wf}{7bFY5D=-OG}%)Q(+ZyMKRH zkU~>an}nEDDI{FV8s-Oqi2_LFRd3orX6_ZvR4UbovT85@laOSJc!RHeM7X)R4i)IT z>K>=)x*PmjK>5|g)_RgVa!pO4yr=mrloF%GaqA~odUbVEJPiI$&A`fW^Ttiy+3>M} z@bE(U@Nn3^QdS~;Wb`t2w@c|v?vsR^sBUR}90!WS0cqy0lrW56}0TCqHo zrX_?DPBrW9=QQa!S8@_{k?&XeUtEK8B@g|7$+Qxy7~cK&OiKojLqP$ugMaI9NN#t2 z|52-0Xs9q<--5+%hJbmE+^^269D$sn(RFMKstlm`p;%e{`0@oBaw7k&abvort%ZY* zhkistHXobq0~XG*va*(pP$`}EM+kQ%WiBp`^_}n|j$-I1fGR}gXKNUPxzz{!4Q5+g zfhN7#8_b5c0l$gugUS*)4z(IU`hvuIXQ9u(NqTuAdC#>)4(?GXxY((bSDD!!WRw2g z`ngo~uhx${$-6)$9bvoB5p<4tUGM+GywAfZaa)o?(Q87v3n&qw{$laXMDUxHY>@Pv&xzu ztel%uf&ByGa`A%DB>;18T-?+^`KIS3mv^XWn2n?ei>01o!>#=d2YoqGePKy6H2>6; zq+LrRJ3BSC!jgIg${=VI&f_Qp`@xN4N+v*FP*}SZqBGfzu$^vCU$6$?vpYQ^XJO&w z1o2Pva9G;YH-nvDG@PxKCJ$#8q_D6;y;oY!UNQji=9iA>h(71=P&zg!NH%dvSXj_6 z?|dDI+Y#r~N0Z(ezL>DLur&t+KsBb5VgyUCa$`gUu7rSoL>ELrUg@It$M!$wHb~3`Ey!Q;ZUoY$BP|s(UH-roij(Dink_UhQ+(E)-VWllSWc+Zm4}dEpcr;xJh-eIR7inyLe5 zMp`N;iC`c|V&nAKPSrQfJZiPd!=ss0x%O!Efvd9|Dz7-#$xoh2jnx}4d_^LZ%tF6_ zt;sE)%1o(WJhdGeS^U>`7Js4elqsqxxROrYWTeWd_g|*0p24k5!AV8R~0lYUwRlt{lSPb#QLT zZLG(`ymIgzJ8)mxQ71l6weJ;+8lmtvd#9K^zTBI(?(Pn8m?*p5CqXwKti7YbXE&99 zeir;!a<8vg_g8Xn(sQ~s&g(w%{y><-e6dyG$jTVgaZd8*2yPj^4~&HNSt++B!saB$ zo(g*`B9)k$Z`#uT1f5h@Paklp`1B4BVAF=vQKHu6A?f}iC zk7O5jc{n+*eNfj@{{pZhv_q>d2RZT`JVv|T@ypjgaC1XPLRk+J(tgR+9@l`&Vxu2J z^{G_z&sT1<$P4Q*f6W9*MY|)7)TQJ<(|oJNS4aw#P9JpNM5TUelF43zP!1}btb5Ce zH=?hyO;nmVTL>HeskDxHL6*_bky~ZHarPL&!V<|+aHi2VoHDNqcqazRR?9C+>Y)TG z;t;iDWM;4x8H$L(M0AgM?G-_JWi_$vz{0}DAgb9#M&%nfZh0r!m2zI)&^hye$_)~{ zbHGy|0G_ly@bHL5GAk9Gx~ub?9v-d5OiqHHMpii-j_YCZ1ekl_3__u3w8T=0m6R0f z^rlMzop-hPQ%ki!^*DK19*?2!0F>a5CaT2*M8=BD zgEPIn)cr}O`kCz;wwtD%qd6LhCwUL@T?2!Hpjc%xwoCtvfs4-R_4zHRT)Is&cUsHO zTw3~TMD6(ymF*b;Rn=3evkSE(v(EEh0wUOSPT#|r_Po42b2Bq)Th$+pjYzw-9$R_E4 zQ-Nyp4YU4ig_F4-L=eLZi3k~tXDrMhqr+T{di`*AhK#YvE^A-fkF{8AZ(qzqgSiR0 zj;g9&qc&^@3LVUUXo)Zmca30A1=!^fvu#{?7!irtP?PCi^PrK5Jx^na@}S1lUhfP=3p z`N;51OtsZ4f%on%!uM0Pk5c9MN$bH)fQgHqSY~t=_h7FPJ3lfV>&g@g-oat4wAm31 zuf^ijZcVvco62HuWjIo0&=I3v3w{K^up5rebFG*r;7a^eQfa>ERxJ3gOroAy5r)M) zZo7Y}x-shHC^7O0Hk;RskmIg8@8#~zr!4Z%t4YOzGnE&t_e6a=xQ}`Y}n_#jEq|_wp>jdoIG)L(sG$I!tX#Nj>l;%T$r?p zfq`MQG-f^Yh7mbA^ju&2KO2i`UOw5`o}Pvl}!iR9o*Qwb@f2;Ot1v< zB>-%~cqTOJ65vjOj=bkjEj?XgCS|tQcxJ#q+KqNwwC%$r_fD%TkGu^Q?Fg8h&>pIj zfA>{kl;ov+KlxQ6Q*CejW9)bF(at&ixVw3W^cmd#v zQWH#sb9{`3`~jX`m?3stVSj6CrOkN$)7SL9LmyO<$0T_;>o;b8{=|4N_x-%N`U=Sf zjantP^}F=K)fPpC?^+}?2>9Q?ZWYXM#;5QjU~6t}4{8zKZ1BftGc(VlFwi0-CcOgR zYk`PLCc0?tov%B){Z?c7-YSmm`osL#D=l5R36h^mLsNHEqbYOpUa72%!DGX}{W?8sat(tE&yO^yl96FjMvUEui#z!tM>kp*$C0lo3M@88V zh(e*iZXl116yu0xtRa$YA4>Z)23RQ2`T)wxqwjN$Q zMQ?OeSIQctc;E$+t^<$Yx(E*cz`(u&)A$VyK7+GXtnTQVMdLC2s>2a_ZS5pA@1r=b zO1t5jT0Sr1kuuAzy>O1%;^G*!mKxj=Gw?2E)M=ysn2_mGxM#^q zTJOzAjL9K~SXBmc9zWYqUM`OonSPKAeWrbU#ppPke(4Z`T8)|0Y*|W9Onax@jbpyA;1_!dEmQYzoG z-sg+*ySQh5wB>MjEt0x}+XCoa!{ubW=Y})X={CkkGpm$Em(cOb2D67JD7FlJ&hyL0 z0@I;Duhnm{ehcHqYg^21F6nkWEy5Xln39R=E3|8dBah+0&4eJ%^(H&}4Q85K6FoV1 z{INTA6R1#c6iclywF^x0NpH$?h5VY&`_Cw+Wto>&&v|xrd9U#k1xK*)g>)kPKM@jYzfkh&?yNPm8N#?U#>K$zePVL`TVG8Y8%QVr}mM>C>XN}^kjYd^SJ3Z(N%7*S?Il0-4 zCVH&FQXD~HVxc@-T#Ie#sib35bjaxb?M!xdy~dERn|4i6)zzg$pZ%b_gr_~4`t+`2 z33s|dtNKU8P4TDh$4Kaeiud<0BCmZ;OnynCE4Eu&myR)kq*5hA6O>fN$QZZbALzF* ziMI~49t?Tr1SYm_FL$&;%~6wm>L<;3=-gtsfuaP>1so%S#dh~)WJ+S*D~Oiffn}Os zR#s-*xjo(;bJgmW-NIYKrJkNRV1{ zg+(vt%~_ZYOT#=#!k%5e-u{`2$)8FhOa!sTZ+%X9iUfp*Z+0O~YJD%5NroQZ&u{wr zutlRVNw4J8$#8BO4(IBjk}VI?d$0U^&z0*MRGV7}V<}RVr_G*yT|SL=Jm8|X3pyCNqRlB@=M25n0`-Vx~8f|St03ISnC%Kf=G!gHmB#T3@ee0l z^n{8tU;7tR9ki8=A8cseFsKuJBxffodSYk2gAOIUj3NmqIZhiL5|XfGU22bg$>2)^ z4EN0^X%hLyuv)}q{lw#y=gX|DPLw&_s$wenOJ#TkrXsn8lkH$&BI_YuP(l85?qG0+E9*zd3bD_j#m5H}l zp*SvZ>FM{Sd9|LHRcxM-;dZ#w6XoI2VbG%XAp-X4rf?oi$4k0kCWAF;m&QF4UhUN& zFpB5WO5Yu=D1S@Ac!EYyJgzNGwKNI*EPzs0>@d~&u2ra6-M(f_jH)&At?!QQ%6`$_ z<;x`E2k)>wIa&Hz71i1*D zV8rwyzqtQ6??9nZGnD^J7T0#)5&fms&KPLVyw|;QTEye$jbS=!$BEt*u-^;?fNemS){>Ej!-s?l3&2BsDRo}>&6 z3=gQ9?Cq@^QBw2U{_@@3-LOoal-ap_iR=@34X~R1Vi#ij8@*LgX+$!0aBkn!pc4w#b9$WcX&n_S?XR@N9WRe&G1E7SYKYf)}h0Mp{%T5 zj75~c5Y75tFw+mV2LR&&ZE&}~m`pv*=yE80l^=Fn;q^WJ@zYEF`U5X8plokF4_D z$j5gh7hzp`8^{`6&Sr0GgkIgDY2)LMgfD(9+#8=8`-H#%J`fa{<}w^>Hn3^dFW|5| zG7qPa+jd*@@?tScRUPT&mao6qqs=tUs~23_E7woJcev@@X)7&PMkecL&%Z(^rlIcW2+6xy@hCk)_;V^V2J&^~bL z&~H&-XWLfne^c zUu|WP^;DBqZ>))ZSkpw@4?oGq2%4B_bXOdnQB@&qL#ng_&-*z21tF(1`)zf@8K=~9 z&0Sz8e_Z_hi^u&y`-QhNJ&9Y7_!IW>r6ncLh}^OmV!-rQm%I^sW-feQC`bR`)qtg? zRJ;PA{J;ReTiCSWCIu{`7^8mC3j()Y+flh=({WxWV>Hy*R0T6D=cfFL=}(xFSEz@P zQW}D(V5Uh2bX9gdyl~}~QKuyGdVnOvPD00t(B;+!PxfgS7X`c1hO_hrHn~!(7*$fL zZ#j8?W(vAtM6&UV9k*UQz-5Za?CdZoenTcidg>IW5Idb^2Q8ZkocHTre`_Tc5Li_td!v~7fILy!tTSu0X4rKg z*TM}jrsK@VO3%R=w!gTFiIX^i%*`biS)U&=sY&Uga@%X}nJ)Wr+;hlYlrgxtxG-;5 z%Ui55B)GKA@X+-UERW&LC&`maj|f+$wcHH!^_5fA^0VwT#|&@t^RlR~V-DK=BqSQ2 zab%w3VrFJOF)R1dc8&4_r`s2apMHUp3R_OwlZxOWwq857F{e>xH}uXAmdd16ks{;4 ztk=D6y~cA2D|e0OdWp!XTB63adRhmYOxFq)ynG^t;|_GTGb zLl41R24Ajo%`=wcnZt##3aJ=$8P#T2k6l3l1=^BIgqcJ=Ust8N*rNOrjr076@P_lI zX6u>>3076=Svfh!=X$mV^5@~3vLE`{a6xpj=|e*K>9Q?z^zJ>3@-4uFK*Cw((6@RJ zFzm+e^jULkdQEc?7VgoFMO}yCgr*NWDIgqs%>VrxG<3?=sfM-sTi+N1k(l=i7Ta2QnIB*K`9d|5+n!9y7?(x zQfNa$dH}XcAxN6%u}Bbb$a-F{phXvR98TwHg2iX{<*yI&th3t8JiG)i}pu=_&U zjWl6LH|OK2sT&@c2D;NdNZ~OEuQakzQ#WYo${JxyDH*Vt@qux=f#NT zmG0_ZRl?Yo2JX@T*ks+1j*aL8f;=8=J@+~G;F-?qGib$z7Y)Sn}`53vJ~;4FFzjmC>1-_S3& zb^c{bK6?!#N^0c}qrmRtx0{a+4%W!7xM#_`DOyJtY<7=N$cxe2~J>& zzT(lXm*(T^>p^;4PhVMCnfifsM2_+akk?y`XxSZUtsAV&d7WQ-f>es)QBjj3t1fgn zIr0L@LZ6pH|LoW>la^+5nRhABHK0{>f8hu*768SQZs%~Iz#Wn82Bi~vUZXo_si}=_ z70od#`6Y~$BqnwBiHj=?1zv$`K(YGK-N9i=w^o0$<4@Zu~v(rmUN;%p{T)eoq&wq7(DGQo;awl@_aKN>yyk`b?x9rat2$YDRMQ%!BjfSj+duzI zDKGz*06;o@O-qz8%5*X%wI`#fq{xq&{ZmFvrnwSGJ-uR_?Uwd;mYZn=Q8phQaR>zijIrfX6KWq|Dj`^ zQDb*0(P%>Yz(j)86rSQkRymz*Q`7K`-u)zVWPD45LUJ^7s-kfhQ833)iU<#V@GM2h zxrhkuZDt0u-R)Msr)}G{z7gi;=Huf2X^V?29;z1#Qgrw1Ach5@)(u@y zuNrD>fJ6;lEa(h|-BGfePtW~2c6}2bX9EY3BxS~~<27!&$5*aPou&&$Wu=)aZFPH{ z#MJw}eqFOR<*2;XTW;O+@!M!Xi9JA8QK;wH86c;-PzJ(vCY6+1mu!+U{ryQr;@YGm zIcz(Zwe=cQ+3}D59jTBmW1(wRd$6_$p@py;y{P2#hfirUKcyCyD3cbD-Q#&Wbtru> z+g+4vK}!e!`Ve~%5Fp`4Yk|(Y4;_p$h7cjlAFwU_OX>w?bbQX$(X^wmT;YfxS(w;| zK=TI4g`2;<5_CrXPs2DDW7MXAOhLrB zLK;a;z013XxAiBAjXi;SkyW_ZOCirzF`VnP5p}vG@csJ;E+b}-nYR4K@RKy>2#zT` zOh+{j6igc4I&^ilvjfm3zr9<^arxup#%=*Sy#5>RWFz+>X;B{V!UzYhVwBc(a0g(K1a zMV@-6P2x}Br)-dHt;HjQbYsG>^jOcy$-WZY3p`XHt9%=~a1-~v!{gg}cwzdl@`~SA ztIqV;LKB`3_oDs7cd!w6{l{AjzpiIIr470rtjzFVyrR>JR>_KmL;204gl?0;&({8$ zHF2uMl~_UVM2zh1$KJC>b&sDBw-h9efGRkW%*@dLn6-<2CgqbMESPiw5)%PZ|- z&OiR>os{H5(v#E?%}f(U^H`BHf6MKt%TkwclEIcaZGSowQ7Jm3tuRr)-H??Jl4tR|zFlz97_PA2VHB8d)i|M@bG_pN7!)QpY$smM{L?Vt2I zu=$a<_2wW)ZmUI_4xm$9B(N2=Uw#rUcUmka)%i0BBDXmGZuD}&D@-qS_P{`^8^oAm zrt|LIX1cO5-Q{52y}7yOw!fWC3b{);lZx9$n}Pcrk9mLj17sk3VxFFYg2Jm4bn+<6 z4%s2Zd7)kM4)~~#RCqfy_mD!T6pfj8Uye~KGX)!aj-l8+9nQqR_HWQqRW3+D%vXu! zaXSqy!^V3x)@K6@ao=CYkj5UCywJG>EqYrgq&tRie91?bQR&H ziTJ7H@)_#4+2hwZTyWb~X)w{~f1g?hpDq4@bL)~a`o1KDq=@Ik7w`!pRh+opY#;yC zt2Mp7z17v#m6ZytfnBR9j~i3ZUt>bS^~Nu)|3{ ze}|00)^K8@p|u}*nE*4hTRoqU(8fHy_QACl3Fd4+TT;>TTrsv>Ps^1E1Zfdfz zqtNluMidHqd30_AGuxeICkUaO2>mUYmKjoTV+x4>Op7Ksnz$S zd?u`D*y!1Hv~bg+Lj4`-9J59?ht1cgKM@-w=jJG;Tx(W zovwvWc(o(j(|TGF&fy|rN@orS$wo>RJM)3nnKl4Hgo&0j2R=D+(pP)bKk78z2+vf0 z9LiDxWsxdz|GwQJr({Jwr+p7AZf*iQiR&C^bnE1pvjSnOBNG7!Mo{tlUEo1?)x_I2 zV%b}91A0d~b^mCOg=#T}G~wvtG@Az@(W&EU<4QIZ_C3cdyGP>8^p}plLToLp-~trU z5^l523$M=$>VVz<<30|({e%4sO%(c*V=gd6;_Ccp4~5#%Xjwvb=b3k}Fg&P@X|05W z)!sEK*SCC%8IdwYR@|}o2;2k=CADcnrQ*8;1)OX#t_z~Ki0E;nA*;mVUZ3dH>gF2C zVJe85>`%K^w_RhOs>$%zXB^$=H-P=BF6RhGmQdM zw8MTBzf?R27ne)~Il3tD9cszFvoo8Vfi;r94wVs86xIhwC*)+u6%n&s!d~B& zi3zndL}q4sKg*AVK1v+&^KNWv+COLr9*wI@>N4h0>FGzKbEjM79y}-seD_gsSjk^1 zzM;7}KPT@NJ9oLw&T0aSdKi8oqE$0NYrI56o-fEKv|&V^i3W4p^4+O{yD=n)cj0RB zR`De6?s^*wyBT;zh`sElV&~tmdy=%ZO+B_jzvQ#F%o>BN3iVN#KP>Y z%B1%Kcdw5&9Ig?3CQA3dnx14~W@|;`H_P|$NtGcT!#&@M#>yLUxGSM9jQawGTHKd+ zq?G(r1_pMx!1hQ`t;bDz_JH5>t6#6cN1DHHK)4HEj6L0-zzORE{KZPfkVVUL~GY{_yme|jKD_~i}D{hHKtr`E{>>y^@wZ1%OMn{2E2x`u!7;B-FZ1@dTwk+i;WK0~v!Ny>#XF z^(;S?Hl;xgCPMW$u{1ZW*=`O3V+*iyV*37kqui}K6Eml)pkUh&a$_7bZNIT=t0B^x zXy6Zl11OSg7K?Sd=JsyfIJLPcjf>zQRAsyShae;|F93?)liJ!l54u-YlIcj7a|+a3 zYSs3{83N&y4-X4FD7hY*%)uFR=8y436UFy?)!}ji8PL919+#fPk3weso>%B<3+6w6V6P;c%uwz`($bEKRHYtO_~xcl`pa~{6R$EtwOcLBpWl6dRN(o zcG(CZxy?<|_NM5dxcV?iPypsvVSb#Zkq`Bvl~Tv(F3=MWa$UD^m{`*I^JKUCr|dt2 zsxa`~b8haeyTF7`tQc8z)u4-W?4b42u4$SySE=E=uur)DDmQ7Y2% z+bUF{e3&vG6ZAfCD4>dCP04JkiYnh+{z$4*m+?9`JNvDV&&dexYge!Gn6#@BYFii} z)~4uydoGR7kFU1Q^NJ)PDn-rw-^#i4S3kIAvDviaO=&;dC2=XBlZ5arr_Kj+CGdj^ z@k%eZTzeIfI`H@Y#-dxW80o)q(8pB(%8 z`dgxS<0_Pt7?n=YrVabTX$agegAm-c*8>X0@dgW0H^!04xyoD~P2JxrS63G_@c(*) zNW12nj`3~hMur63I(X6k>VLYLTIb;biTT{b z`zwh6vY8I#Os*-RoDRfKLf1}aW)ITn3m00WPL@CA(fq`fs?2eKuKrHMh`o1=M)mfC zsNi@7!NX_k}hjp4&gG#n3&AvZ32JiLB}N!iVIVf#(p>@3y< zZ3<8v!01i%@LbNARrj(Pm#dGLSfFyU@=Evjf-wcA$%}$}Cl{v2>#%1VrF*rmcx1Nx z-DWVlze0QZwD)xLTG~>!%XG%w*s9C+n4IkF(;^FfNVm~aUZ_U~NyXE-u0l`OS-<88 zt6RKKB#XLfvjObBv5$(r7ek%6ceKx9ZeI%oO+a>$Y)o|4fXVOJp>< z)F|-Vmzd^A@NDR67T?Tv{d(Ql$u_3C6Xg zz}(8prU}tt_)-u%>#&|;QBE|x9z{9^Rpf40q-9UH3>--SVD0XFZ4*Aq_kAu6xY%m8mbP<$I9Y zlaaO5)?OGy*5Q(bm71gg4W~`8My+pX5wg4FZ@ zRI{EL+4)U*2UK~I_L~r{A=f&ErZKf1Hy+8z$nd1^Fh~S)nsrBll7uB(z2b-V*DiZg zEgzQT)8{e-5nlc~I`H&>@F4Q~V3;ma(8ajfUNBx^7EEu~EXpPcn!ypzWzAnWML8K1 zI09!UVFlN>wY`hp+by-fM!%w|@E+y`Xj`nUZF#L(yJ1w&N<#?K#k$frGA~Vd?-zF< zZ~)@S7R?*)3xxzW65>X!96x`#gBLGC^EFhcsD=<=q68jxi-;kgqCXuvO2+w$jJDRc zqxR4$^vJ6~$t&6qs4RN(9Ou#=+heXGoE)H`dfENBiiy1~e+db?-u$YT7G*;Ai4K2Z z!;ZsDl!Z!$qPz++i-VGKstvVYth>g*qhdfWT4!f=a=}NDFhQ;W3wqwWMGre~#W(aJP^>9?<569G4MR>AzK=EUroCy;tR)13{G~EP{~2~15Y^Ps z(rtKa#hxrrf^emFtoI^*u5~~x>3fcn5mcbh&im|I?oT8`_8cVjS>$zrM)4$z;}&|T zv|dSPtn9}|RaI4h2~?=ARqZ{WNF1l(ws?cGmPl4SPM>$}nNVuw?juo=4?)hu(DT-Q z9lUeI2!RkEDNJu;>AGqe85tFbNt`)!IqXbFnhaU3u0Osfd@xaIff96$M1w{Da`fI# zH#osOfq1IaxHE}v1SMcir;U3mw5_aX7Iu4&3`GX4T#-R~LSMetOZkidTV&a#*Xu+XRS`q$;mPv&BNZ#q_J( zn>?8Qvq@*ONNRa*W>lX(~ z5t#caAs>2^?jB$*7H8En=jWLM>v8AhLUk+MErb?pC=J?LSSzN!Lq8-tXLT!GS3M&# zDr%@8ioYQ!J7wDsVe}y|bNb6VfM5M85V90+W$JeTw@B?mL9t_McK`uNx@wLpLV{Xw zO~3UP59fS)s$8q<-BX*VfP>jxYakb)mTwi@`pOkud@qqnC3EKh+f@O};*R!G;%A@p zJfq;yIbJt=+*7Bu{JG*~rhzso22UGYYj3 zwjJA;Z;s^I${etSKg7KJ3AOK}K&Wk?zSNyA3<8Mt-H!^%R@T;_&XDo(Vbv;`<(qp^BDSA`bc8&8Ax-15g?9@snT+ROe6IjKS4wPCT

TR7P)E!##vfhM{U=fH7jQ~sw~W(=RQHVCI*(K@oy=hd>vB19+RM~8fHXZ z=jC#+m?f_pBsIAw6X9{nn89PFCv`f_k+{-+F=I)d1*0Jzb)FVjjrD++2+PWCB`N3M zk9&nWaGl%{@`K%DzSiePU`U#3AEH8n?9G*mR*UU#zhGCud9S2vIAdqLn~t?5gLt~7 z4J}{0oQ^abGOn6ppGsb7BpwH%4@oTF!@TR`(Q_mk-!2FbdYWMq72X;P8)a z(w)#5YO=~dqN#1(wwNKFB&`_kbhVW##ns=++GmOp0UCd-LiyaU?`C`UG)8ks-F5kg zdn@f>>Z+94-P>ln*Z&o~BH-a2xsDHBsZ8%+Iv)l}?vq@?Z8HQ9$;d@;|9Z8pHh=gg zVWwvgE;ho|sqtTkMwur5v+5N7&wfoRAD-8&a-Y@xhdPs!nUr6hqBeax%519~BDL}C zalAsRY*NsyGDR^_vUFxvXYb(P7df`xGW+5GC{&b`k!hs+A3-9R|0v*#tMhH8ivcmi zew2eOKK}=_r80SB_v8bWS48x1rh1anf4Te8w)Abr*pk}aUa1p9FTxNnN27;Ix-yg= z5L#ZNA2l^y_~L?CSX}fV)$%#l7jJ-pR7*NIrJ^t@inim|6#D@w+4(bM=O-4Y-By}z zm|fv!aB@^tJ$!HL>^w*Cb7|TlA1|NrFw`r+g~H0ts-3mCsL04Yj8W;O<@ZnjiU%I* zs3{|=$I|K8{M)&un0(xa7&5@yWx9xQ07wecVN^&jxm9C8LIYB&Q`_HAovANZ%J4aTd`y%ALqzWB81wYFFt z!8(RVuQrWkplWy{twjUhRS=5loMm35tR%#UqXut{JuUiz?Auzp?SN3b<4r-Cxwu$n zP-n-0Gq`*4{-yUAZt{E#`u`nPD2zCb8yVWqBP**~j!nLa?J}zX|3QekypE}!=^_*R z`$n@;kLuSh`1-lUU6G{R|A-thdlaWH>CO5Ab_W^IWXrMA17xSNF&sBOnrqODDp*;0 zb1%9IIrTO)q(DW~m*!|R%3%d~?NA^G@)6B$eVhd(iOjnOYau4v5+r^>24Y~d#+vB>r zMAthNjr|!=^P{h62+nOQ4~S^+Bn=B=oWl2mB&*c9g#$X!qG)nq8JWoxEb2Nt7nd)d zF7r{*vALE0*8Y!`Ve5Ba}cPZG&_^e+LDHLE{poqMYLQ$^w2rUNJ*lM}Ot1qDQ9~|mfg-P27U&i0} zioz@0*CSHb{yPxjq1{>B1*Oc9-#c4g!N1DyyS8?=^=}d;96N}djJyx;^7!!F_uc;2 zZX`hFpqN9KSWJ9z_rW7SWFq^i-?d3NI08?mRtB5xg>b z&bB{L@3Qq}Z{vi`!zb2z;=dI#0G!^%eaa)DmKj`tFD$7b8xHO_;Ei>5I0Oo!kFQ|ozbNM zz{v%HxOy&MQ%A?z?gkA(+$S5N(ZAsT^!4`6a25V_#a{bI+-!$qZBz02!K-+tDi2ba zr6R9^ms7!~DW+m+pMKHwWXqSP!42V*>}=>s;prLwgdufODK|t#W+>$^`S5c}n2ubD z=5t&gq@d@}$Tr?M2s%T6KrsADoid>O+kJnVQX?`7;;VAQz8o)Tgq*4nZ=V@`K`GT( z1q#^$H>Pl`?nsnVkrnm=XV9ZxKU!2Rb1*UOTV zlcD;>F0aBsPak_fPP_>(HN1*8aXl4_I~58+LB^&|O-bc;4TAq`5^*3^3xBehLR)eP zQgKEO4sO$a@V_5SyRWN{gP~uLVK&awcM58MDv+p26fzsrTiI~L^l!|A06FkIM&-GE zj4&dks3^?t-=M&?VR<}|4anb*-_Hl8B~Gz?&~88poadvPaG3+h1&@Zb?mgWt)tlf5 zTPl{oO$!Iaed-@^-~4}2p;k$PEf$;LvGqG2KN*i;obxs->DVvnX_P=09+I94{ls z$0%7f3iNG1BqmO8%<}+Y1U92@8U3eEXFH$;TIVi+Ma8)@d;(TmtUx3#JGLd^ihA0T z#u&_GF_>e!KT$71LK`G`MQ+jhG|f=11-8=B+PEw2MzB;&TygYeVOsUu_OhTE2rGH| z<(j)!o@txTbODS11;KMSmjjPri&?^X$VH?-2#BRCXYLpW+xF$8qBJ;7*FOLaxW`=3 z!r&L7-QMiddlS)wV&b*EL|Sv)@~RtP=^m7u&6~w95)gZq-}RCfBfMqU^Ns!~+AKMg z;Ns?HXV1>c3Tk|oUuxXFnv$&Em2Pb?%qIl9*T+MU{rI2fN9RA1D4PTBNz&uESEQdL zdNP=l)5~Mtk3F@(uCK5-$3gisRU@_ATBWDs?+bVvl+6)WuU?}Qv|Cu-3c!~=M(=su zaz^c>3u9BD&WvsW9ct0@m9Z7NxU}?i0Q*G9$etR3L2@WeP#kcue|AKJTm0@LT>8Q|DbM*!XDPUzj$D%sJI>cU#=tPL(F00PPhpU+`c=fC&knPgW;ET{&d4-2#fkv3b~;f;?12fauJC-2HzinXtrqgFiM*UK^+gPOxZ9uDPLyr8_h^}A-c_rT=`OyQgp`%8lUCt*Gx4>^O{X9fbTK? z?b|kU-wszXsE7|A&d9s`PJXcd&clJN3ges!!+e3P{6~tZ*28r+lwZuP8@pdec~GhU zM4~#!dFLDX#<;(@crC;;m@8L6!2Ah{?>@Nf;Nhh*C8AiRlk~YR?D=HR*d9I1HasE0Q?(iv^Y6~Z*Nle3G8 z++#GKHx?`cl}bkD0TgH0?It8)n-+NuoGWhgGY*GiHB2fQ{e6ADKwbybl2ivyhBjgB zr2t9j>cnqrD17tT3(-q$Ar}8$Jhsp$!;OWk(jeEa%Kp_g4^W?`Kiua@Aoc%rW<`F7 zxUoUm*23ks#f06H!OHl??zB^D>+7Yi2en_4uA7Nm_voeDIrxP)H$^d{6l`Z!0%6~? zd3Y%R#FmnTv2BLtgQdJqgAR}$bhBU+Sx|pbRp#dAmS(^W;)=(s_+RFEm^G*wJeD`t zs6aYNaI|}cvH0!3$|1XSU4JHdCYUSzWlm+HazDO(?!pD=nE{ou zj3n&g*RPlB{OlJy1M2bRd5>=w|FEq(3pGa=0DxG$(G6RH#q?#*-QZ*giD;5Q^)8aq`KE z=VZ*aJpqn1H_r%;kAIWB0`>_!W_{uIi+&oX4Rm!WsHthV&19vd%)39c|8~FsC{lf_ z?HLsn`N~bmX2IhPR={=P@lf334hBHDWsn7b-lYb=hpv{t&1NRK1 zYJbE+)Y6T);uWaN9peg|MRarqWHl~riGr$-T?IFM$YQ1cRtd5uZeCZz@}$_bdG4m1 zTk5l8KuLePw^ZfR^leqi>62j#F!x-#4Btk?F zwFhH3@uLq2YW>a!fCoPtht}G9sdZ3W3Oa9RsRc8Z zAvV^N)RamZ>f7X1(scI<#!-<1V{5U1_6tBAsMP3@J|2Do%!CIp6CE>@<*ST~5>OU@;|5ud4|2>%S|2(b3vvgPm5%b5ZYwA$bS{PhPMTt+z-&b#Mn7b~% z!iadC;LghWZ>YmGMYL4N=d(E=I2I5T%!DOyld%D0f1sbGWYe-79jgJ#jmT@-(Cx;nn<=|kN#=P=qiCxd;rgCc6-X`6LTiQ>wJ#nE0-1h3< zlX;!Jq2y(4Agh&U+E?X8lB$&4UZNuBpQ7Xm2#f7v9}@c^pvrA$L*?@2IDkFG&fw7n zSPATZvl8+YYTjycnJf*sjeN}>r|r*&l#2D!O-|h3-g&bUZo#O#ebj?(35;qQm0#7= z4A-}`wA8mHDKh#psFb8gAX(?R%}X6-6%tR;MyuiBKB-qd)*cVHJl^zQporPcdlOm% z`e~D%Op;S4Wu(frz6?Pi`gm$ezh4QoR~+_VE(s6wEtzM?vEZSYD;}-)zgOJe?n%qD zfQS4K2YV-HhS{);zT!BSn|NJ`d&hBj!=3tzdW29NL}Gexgf>P1Ww8x@d*q?hS(Hpv zJ_sQTUQvdG@??EWMYDkPoq!#|Omj*PGXFen$o$MhiHDJM-KS?QW$gH7u3ArHj_ z6;)$X^QDnc9uEEeYN*o6g!8!oc@REeQ#E7(KsmMt&TLvmR*SnQuhp$~Fv#$^jaCBi z4bcLN@b=TO!fdekAZ&wb)T@(ob()3G$7^MHcmSc4=mrw_9Bz=XOWAfx#jC|`TS(Ji z5W6uJw@MWfoIZ!%eUAQG`j~SJQ#~KW;WUw`3zf#?hS3{otR99v3C-xA0zkRX$;rv2p5TLu83tc+TOw^_OO(s}HX#^!>|*?V1gsVu zdSq#TM^`2AG{lE!$^V*bUr#UxM54Pqp#E7xLIN z?NV4a%gJ&dt~2}>e&BGPdHN_t$)99{O|*Kr`$KJsC4tYo-Nl4TykN_ z3}?UQH%kc}h~6P}f~6?#5s>H<op1qBEC+pBK6jR zBfjTM!2@niV8Zfps3Z>>xSGY3Ja3z$Xq?~ap%6(@OaZ-xmVlc%NT7B!anAyP$fUmm zBEP;{JhWP!fC@b>+dNH?v5V5= zFOo$wfh=uzVk$O`B0F2?TP4)F*fSbb)q+{QUD5@6F5FzNO>vIhif}y9R$Z1HF|I~=wOk6t>B%R@=HQ%rXU|v=B`7==qz^_Ur~*%wX0GJ{REIU2co9 zsrd_srcF#=I%0E?j&D0x%ayMmLP&B3xi{Y4T5DY?bf*?lRbwl&+H`>ev{+aOf^slb z+NcholW;!VfgGD&^yS^Ey;7*jfGHiFFJCX1%0ZSrJvDWle&59f>`O0u6k2VhM5UW` z)cPL!dGCR}fynDeV4bzMLouX}Ci&$1S-^pGbtQ3ga}#7wo%-&czV}|z30SCjl0tYo z#e^%Ton2xP5-GI97j|sCAr<~p(Y{3UA{UDc;poWI7}NroF9GwJU~4fOi6j?sW;cEo z3Z4fF3Lp&hAT2{|I`rfglYY{O;6{*$;ym`6n$k?eI6(WXlBs2ZlTrlyWijYDJ**k! z5@i-#J$DsMxlXy!B@Z?H%K!+Je_ z!Du{r0_>!G3~+byEy&2oJV~3?k@Y3B;H~5P(@%d@;sT&|nndVcOk*K%9gC?Tfcb7A2PCf!MvqwVpR`a4}n(;>Z6S6AO! z=$5g#x#oXQo9;;j>C|Eh1HZQg7tDxz(-J8-SIEr4PlE3 z47A=t3!1f%)6r#~6neA=raf}#Yk1S1J;y8NK{MHJc0cFHB)c z%=Qkr-W_n;cb}jah3#oYOE%=(eE_xLQ>R|4 zjeSjmJ6}hNR?b%%fEYx|G)~HyP+Ee{SFM-nMrhavnfm!LF!4`nib{d;L@J*&#l+&*7WWgYm7*)X4OO+vT z2Pmlh(HB7sall{Py|AJ194-#9hX8aNg0q_UWg& zZWljpz}mssi;7v3V`N;e{FJ&p`b%t<<@IIvwWGLTS+n}lQr36(fsI3)`#{-X5{1AwmV(@GjZ4pFk9aKXYyai%SA;+K+0e2 zEg$;&tu;RYiiK`M4(~QyzyP6C%v(n`CO}fu-DyMI|Ey+#G&Q{_)K%wqcQnhbCV|c` z>o=Jw6K-=@=?UZrsGh62E`O=E)Qwk2E>NvHwxP@Rq|kCaAtGX+uRWe6r+&}qPx&u-t+X%~@ScFl)6PNo$~yybvMcjny!tXx;ZZ#`{EUNs9C zAuY6gHcP#+GeSK7j-9(rN(I9yCL6`uk?q0{co6sIKFs?bvu)YE6`WOgV`@TC(c{x* zQwJA|6PN)@kRzb`53RUaJ1Vttx)N03e+%q8y@XGS-oxG9JQ#54b9oLrn za6XhGpy%&j5G3Vwss?w<+#J0Ih<7x~T+477qj>Q@XeI!P2TN-HeV5O6OjTZ|jJ93Z z)ZGpNbko<-(b3kv5n6BT*p-H}YtuG0jTAO?h}m0Tkk1uANiPakaVlwQF2Yx9zM#>= z!xnvSZ_-yofI7dsdya;tCqrqzsVEm8>uAt$>ZrDv%?Z5*qB2&kXm>s%ir@3+4Y#@3 za(FH{=FUH1Ig>acSbhB#ZDVMUx-Hv{dT7PhSAV}`23qu-G$xO=;_mJ*lxE=M;_9M3 z9$1ghKQ9Aiv3QFWUvKE&f`hD|=l6Q;kj>aoHNZZ91rPz1W{n^wwsq1p&$1}_31L7O zv}=He8V{~1;8_P-L;-Zn6d?p4CQ&vfSjtygW)fh@!soLJr}1wx+GQ_2Jsn{;U(18j zLI4Ip^y<_(MQ2}?w1;2%>Tj%$eA$;6_?LYJ_w*Zo|DIzp$p^ow3T_#Hc46DWx`%^5 zq(bs~BHbwv;-{)0{}P_fs~GWK?*o3g{os(?{(d7E^+oWTK*%qyf=y?a&CGeE9RR``Y|_GqbHYQ&pjPI5mIq(0t$+nM*rafO$CZ3;yT-)!mu@L%IKb{G2*1>Y#K=WlK(F zNkT|-n6D|x5@k)2N<$1L#y-QGib|0tNwy}9ok1qXuCkj!jBUo2EW?;F_8H5J`!k*I z{e3*{`*HsTx1V^pF5~)KpY{EIy`Jykz{ZS4*Fv8aCU)yh@%mbvRjjS*hdVYDIe116 zgQr5b?6WiEp>E&{%;%)OR;vf~MYjtQzR)JxWM10&90xRy!dpNB*;z77 z?jQB^GhY|pEUhd#y6Rw@Y#S3dIzV)w`-hYLAnmGeU zTkx^rrV?)P3Bw6#m(*yendc`|l~Sy^2e00QHFnj-e!@BNNMFNSyK2ZEU%udfuJAD~ zZ{B}mlX_!mjDUT8Sh0RGR>l6c z{n5n--WwR_poWyfCsiL|jW$3$y*HRpt?Hq>J#ui;QChfAX_0gsei;4MvGthrSkIz_ zL3gIR!Ny7Sx)bBAdEpS{`chpTf>*q2cY%262wKsVyUn;W>_tuDH0SuD|Er92CN(xR zm}S+2=aH$kiPw3y zp-%X`-96}3mMwEdpzEl**M>1*;f>VBtZ(JyX7;o-&W7&c9YNpSJ+r@>6V5jv^y#(0 z+tOWlB*=0C9k&Us=x=DcLv8Oy@VcEbCli)F&Bz97sGnDgxX1N240K>k{8n<@fjV>D z((UA`|CqqUtj**Bd@`U!{CYCm(^ZyHMRk|4DHx{Y+7G82i!UO0^5qF?N5G=!)TgCD zR$J~BGOt<~XvgfzjXOY{>mqw;qdnC2Vorvz%?T z-v(l?#9?sUy9$TeTCXs}H=Q0QS`_n}Og5Euz`$}B+S>)4>C`D!v-h1XHA`z1%#M$N z`;&s-dW?;_?@yS`)+zs*7JYS}Bq)u@f1?M0< zzlWi8oC)mdmPe0yAp_G(Bl~(19m^pdDqbhk1MM%up3<;6m=WA=MbK$Fgv8d~@tYNd z1>zC%Yx6uvi{)m9YQ^QD9i6%NkX6TO zHz>sDjeK`BwV>?AS>GDVVHIoU=qx|=Nqh0zBI93=F*jz%GXBPBIpz zfFhc%a)wc+3enS6a|lwlNL3$iv}%vDxPMYDJrVf{qmILip_4AetpS z<^$60@?h#(2uZ8g?V?^BsWjEJHt}^l|MUs$-0IrNh|@Z6b}E8_T3m7__ntf3$w+>0 z-X2&=HRuvY`tJE@ll-t&)Z7VGwGWr8eGy#m`O`}7%yR#l7VzGILd?8KllbVtF?IN1 z1S;S*LLrxU>aYA<=?%!S^;trSy-u6Pi75w*wG&O3RAng#ht^MkNyyDL1-coc>W)wX zMoq-nK0RTzv-|9(f^{WN*(6R)NaoKfWn3)uS!$zBB_~=(@m<6h3)ku7>!IMe$)a6^ z{xm+NtegGV&Z7DTd|zPJE>RzT*ZT!VXfaU80 z86F4IPZB!m*k4pbg{;H0E=GG-Hs%oF>ZaQI4AFvfTeB`;{^Oi)LNevgu&pQMS`W%q z7GRlO=*{s9Bh4v;?TytbA9JF{o!#jh1*KX_QCQ>E_nGiPujrTzsT|wp%MrFmcl|!O zj0rPbOvokI!ZDtux+>M-R?h7(29?I9j^w&~Ynvz?=a^OpKoJ2h5H~IGAanU!Nm{Pvk*JXgL@4t45j z0|R(amiXp-_L~P1<*@pV#BwmgIv9CF%>8&Bb(@|q-N z_$S@)HMAVcPMV&swe9H41ant3=4^floduh+#ECX`w*cWQbMq(RD_hX8Orj(Bj!6yO z4@WZ-xCLPnY4sn9ONb=WDsFRGW|h5)^7+ zJgDrSnpc)v)fM}qI4EFoB^S@Rd`N;zF6Cc$U>Z&9Qa|%=^ z?)$H?TM4^-cbPmEflEFoM@P)j*+M1>JCdRKGX)B^25wV}-dc~quYdV`fAD`G6*F3B zR)?I2*d2FfA31EDXU_QidW?qp>xyV8S2lRJ&>sZ|#y@6_>fadvj{v4x{e68i8s({t z+P(cZpf_I>(Nt8{KJK$J-8m){jtPe@Y~L&ToZ)kUD=)aVIV&4tw;vx(H4^>3zsa_p zCOgY>ex_DmTmKm$y6`_VmHIs2*^Nj^sLTeVQWHZhBQ#kSK}1(bebu8p!HW+E9Vt$K z5#HZ7^4s#XfKj=(JB!Cc)`o41J*YxQ?*kVG&m4pzB7uE@2yjb4$XrozR#M?yJxKUU zvyJAO@PVAckKU9w+X|dXo^W3b8$20+4|KRxee!MVv&*yXbdtfIm`@HsfD`zMh`Hqu zWdBQ}mB7GIILsuL_z~U$QUtbQ&ozFY8M8H@2LvMEa5&yjdWP%7+_qx{n9A|72UDRn0d)*e zbKno!u&8P0Fz+}!M>8n3u?Haq0na5R*bL2}#wFJi!@Y~YlR5pz;wZbFWIiI1fU6zk zO)ncw?lH^o`_b)Td}bNI$&BhQy5)4|C1`vsDe2jc_UFi=HtsErW_!3;im0tZZf%Fl ze18-kq%RN3Y$nHng0IGD&HcsUZKW|Yz*=O1Hy}^(Z6Fxvv5Dk>nOboteV?W5X%f@J zTzH?K9Tnv}>~)1Q(!m4!b!mnEiU3xXoAU@(7#H5H8pehg1gRqb_ z-UAhI07H-11K9tB$B~m7asM~M0roUpaqCu@?ppo;vai6ITtboq394E=w?2nf=v=Gm zx>9wB#Ow*~L$&3`3t-!Iv!(A(FH9ix3Swn+LI>JYb?yqolXVg8B z@5zlmc&;1#YagHu96le&E&%O(C=Yx8!8X{Au*@)54-a~p_lzmc?Le>XkDO_M(&m?M3usHOv7D*#mQu614mEOBXvzN=~HLHc5Rw@8OnW!7;& z;xq)Z{sWmnq`cwm|D$UhOWc9cQeOGmu+fm&o+PJ$-&5+}S(7>dg0``VXH5y6_Z-3$ zmg~M-ZNE56?_pupGpQ-@fFkU=kkDh0ds$ZR_$2@8&}zhbEtEg~Iab0%p8wvFmQ^Y% zjdiEz_Sl?w5etGvQ(1V>4VrC8uRTbU5fZ}u==cd6s2^yJtvd)?WOSA9bMRQ9pws>Q z7MU}kraVRncKRIY6VsfD3R1CjT?TP{s34VDJ%m-I@g-vVFb zD39&gT_nu*9ec$c>2MNRC&oHE0bvk$LW@u9DJ)DkiHV6}1z}E=r6vjO+|W>f$O`22 zYpQc@1LMjzpKJv41)!XKtDGt*dJ_{918g>c#y_s**mjsJA-CkwiN6Ef&>*G~vsLDR zGy8Me`Cns3kOLV50L=v3fwjMtsMc2iXj=m%K8bp_X^a}}qwnX}#fLY)6O7Jpndw2u z_P?KMqYu#Sc-f_)Z51Fmuc9JZ<4C%B6sP>hp~wdD>7qx~VStqU(rSC#TWv1I*Y$-w z1Q$oh8M8sp747AcJq-eXjDz^ZkK}7;N!k8tzHt@8Fs>5S z-4M2?M8!Cl`8T{48+&FL*4IEcHS%UHrXCdT!E5e1IyvT!DT7 z#yXEpq98QLlL(D-<_(k`#6*9Lj&JL?Y^p{N{ymikah99QYnfwIF z07MK@IRC_qSQDf%om?{GO^FAN3iW(fJmwFn?koqe#Jk^dUA6}m6o#vVxJ?8o;0zzb zHh_hzqw!r|*c@>nC?&=KkHdw4K>ouF%A>WFUc;Loipoai4Cfa$rk_RIs~hftRgKNA zp)4#eUIY~rP~0-83TjQnKH1`eVHkui-2=6IxOzU_Sth)!126h3HCDbFM_QMMpo9Gu z1jnt+kBH8!RW~{U6FNZ!0R#K=SXn)zv@5oG)*6YH-X1Q7n`X}ru z2clHUjedFQ&)!~mm?v6NkpH*RORz+gSDf8Ej8ZK!3RAoQgaebOu1lduA3myf#?KB_ z227``+g}^}ECCHqkzeW2qeyFfJGFh8vpCT`?R_o?m=_Ce)&m*e8_W}8qSM0zFgG!V zg5afVKzhHWk~e|Y=0So|jXscnxB%lyp}?1;)c68HD~6LYV_YUKu1EgPrsb`p zQeKHXZ=df^9(V)l2h_{Fh!AP(g3A|f>Sd!{{9(%QZtydpY5v%$DEj90HQE2>Wd6)5 ziY)+j8-ud#d9c+#Ou%nVIzP9)SMKZ&6aRUO=(hhiUmCQ{U#+jC%f~kC6{YrmF*UZl KPyoC2@V@|SR1psV literal 66879 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 From 870da906d434caf9e7b396974418dcae85ba9018 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:10:34 +0000 Subject: [PATCH 16/16] * refresh the maintenance window screenshot --- docs/screenshots/maintenance.png | Bin 84977 -> 78735 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/screenshots/maintenance.png b/docs/screenshots/maintenance.png index bbe18b308debf87efe2f0a23dd8a87cd4e357ccc..ddd2d329db634d86dfe7695f6b80f52aeda44c7c 100644 GIT binary patch literal 78735 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� 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 literal 84977 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 I$|Uf=01jUsTL1t6