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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ second with the Engines page's `TransferRate` and units, rewritten four times a
- **Mark of the Web** (`Platform/MarkOfTheWeb`, Windows): every saved file gets a `Zone.Identifier` stream as a
browser's download would (`ZoneId=3`, and `HostUrl` with the address a download came from, without sign-in or
fragment), so SmartScreen, Office's block on macros from the internet and Protected View still apply. `JobQueue`
copies a source's mark onto the files converted from it (also for "Save only the converted file"); a folder result
is left to its engine (the archive engine marks what it unpacked). A drive without NTFS streams gets no mark, and
copies a source's mark onto the files converted from it (also for "Save only the converted file"), and onto an
archive packed from many files when any of them has one (files inside folders too, links not followed); a folder
result is left to its engine (the archive engine marks what it unpacked). A drive without NTFS streams gets no mark, and
that never fails a conversion.

## Donut control
Expand Down
4 changes: 4 additions & 0 deletions src/Filee.Core/Conversion/JobQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,11 @@ private async Task RunCombinedAsync(ConversionJob job, string workDir)
files.Add(file);
}
}
// An archive with a file from the internet in it still came from there, whichever file that was.
var mark = Platform.MarkOfTheWeb.ReadFirst(files, _log, job.CancellationToken);
await _combiner!.CombineAsync(files, output, format.Id, job.Preset, workDir, progress, job.CancellationToken);
if (mark is not null && File.Exists(output))
Platform.MarkOfTheWeb.TryWrite(output, mark, _log);
inputs[0].Outputs.Add(output);
foreach (var f in inputs)
{
Expand Down
28 changes: 28 additions & 0 deletions src/Filee.Core/Platform/MarkOfTheWeb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,34 @@ public static string Internet(Uri? hostUrl = null, Uri? referrerUrl = null)
}
}

/// <summary>
/// The first mark among the files <paramref name="paths"/> and the files in the folders among them, or null: for a
/// result made of all of them. Links inside a folder aren't followed, and a folder that can't be read counts as unmarked.
/// </summary>
public static string? ReadFirst(IEnumerable<string> paths, ILogger? log = null, CancellationToken cancellationToken = default)
{
if (!OperatingSystem.IsWindows())
return null;
var inFolder = new EnumerationOptions { RecurseSubdirectories = true, AttributesToSkip = FileAttributes.ReparsePoint };
foreach (var path in paths)
{
try
{
foreach (var file in Directory.Exists(path) ? Directory.EnumerateFiles(path, "*", inFolder) : [path])
{
cancellationToken.ThrowIfCancellationRequested();
if (Read(file) is { } mark)
return mark;
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
log?.LogInformation("Could not look for marks in {Folder}: {Reason}", path, ex.Message);
}
}
return null;
}

/// <summary>
/// Gives <paramref name="path"/> the mark <paramref name="mark"/>. False when the file or its drive can't take it;
/// the file itself stays as it is.
Expand Down
66 changes: 66 additions & 0 deletions tests/Filee.Core.Tests/JobQueueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,72 @@ public async Task An_output_converted_from_a_file_from_the_internet_keeps_its_ma
Assert.Null(MarkOfTheWeb.Read(Path.Combine(dir.Path, "b.png")));
}

[Fact]
public async Task An_archive_packed_from_a_file_from_the_internet_has_its_mark_of_the_web()
{
Assert.SkipUnless(OperatingSystem.IsWindows(), "The Mark of the Web is a Windows (NTFS) stream.");
using var dir = new TempDir();
var plain = dir.File("plain.txt");
var marked = dir.File("marked.txt");
var mark = MarkOfTheWeb.Internet(new Uri("https://example.com/marked.txt"));
Assert.SkipUnless(MarkOfTheWeb.TryWrite(marked, mark), "The temp drive holds no NTFS streams.");
var folder = Directory.CreateDirectory(Path.Combine(dir.Path, "folder", "inner")).Parent!.FullName;
Assert.True(MarkOfTheWeb.TryWrite(WriteFile(Path.Combine(folder, "inner", "deep.txt")), mark));
await using var queue = new JobQueue(new ConverterCatalog([]), combiner: new FakeCombiner());
var preset = new Preset { TargetFormat = "zip", Archive = { CombineIntoOne = true } };

// The marked file comes second, and the archive is named after the first.
var mixed = await Run(queue, [plain, marked], preset);
Assert.Equal(JobState.Completed, mixed.State);
Assert.Equal(mark, MarkOfTheWeb.Read(mixed.Outputs.Single()));

var fromFolder = await Run(queue, [folder], preset);
Assert.Equal(mark, MarkOfTheWeb.Read(fromFolder.Outputs.Single()));

var unmarked = await Run(queue, [dir.File("other.txt")], preset);
Assert.Null(MarkOfTheWeb.Read(unmarked.Outputs.Single()));

static string WriteFile(string path)
{
File.WriteAllText(path, "x");
return path;
}
}

[Fact]
public async Task A_link_in_a_folder_being_packed_gives_the_archive_no_mark_of_the_web()
{
Assert.SkipUnless(OperatingSystem.IsWindows(), "The Mark of the Web is a Windows (NTFS) stream.");
using var dir = new TempDir();
using var outside = new TempDir();
Assert.SkipUnless(MarkOfTheWeb.TryWrite(outside.File("elsewhere.txt"), MarkOfTheWeb.Internet()), "The temp drive holds no NTFS streams.");
var folder = Directory.CreateDirectory(Path.Combine(dir.Path, "folder")).FullName;
dir.File(Path.Combine("folder", "here.txt"));
var link = Path.Combine(folder, "link");
// A junction: unlike a symbolic link, it takes no privilege, and the packer leaves both out alike.
using var mklink = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("cmd.exe", ["/c", "mklink", "/J", link, outside.Path])
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
})!;
await mklink.WaitForExitAsync(TestContext.Current.CancellationToken);
Assert.SkipUnless(mklink.ExitCode == 0, "This drive can't hold a junction.");
try
{
await using var queue = new JobQueue(new ConverterCatalog([]), combiner: new FakeCombiner());

var job = await Run(queue, [folder], new Preset { TargetFormat = "zip", Archive = { CombineIntoOne = true } });

Assert.Equal(JobState.Completed, job.State);
Assert.Null(MarkOfTheWeb.Read(job.Outputs.Single()));
}
finally
{
Directory.Delete(link); // the link only, before the folders are deleted
}
}

[Fact]
public async Task Missing_fonts_from_every_step_are_combined_without_blocking_conversion()
{
Expand Down
Loading