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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ The mounted view is constrained by the underlying filesystem, backing-stream acc
| [VirtualDiskConvert](Utilities/VirtualDiskConvert) | Convert virtual disks between supported input/output formats | Output is limited to formats/layouts with builders |
| [DiskDump](Utilities/DiskDump) | Inspect disks, partitioning and detected filesystems | Depends on the selected format and transport |
| [FileExtract](Utilities/FileExtract) | Extract files from supported disk/filesystem images | Depends on the selected format and transport |
| [ISOCreate](Utilities/ISOCreate) | Build an ISO image from source files | Uses the ISO 9660 builder |
| [ISOCreate](Utilities/ISOCreate) | Build an ISO or UDF image from source files | ISO 9660 by default; `-udf` selects UDF 2.01 |

The source tree contains further tools, including BCD, NTFS, VHD and VHDX diagnostics. Consult each utility's help for arguments and restrictions.

Expand Down
1 change: 1 addition & 0 deletions Tests/LibraryTests/LibraryTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<ItemGroup>
<ProjectReference Include="..\..\Utilities\DiscUtils.Diagnostics\DiscUtils.Diagnostics.csproj" />
<ProjectReference Include="..\..\Utilities\ISOCreate\ISOCreate.csproj" />
<ProjectReference Include="..\..\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\..\Library\DiscUtils\DiscUtils.csproj" />
</ItemGroup>
Expand Down
138 changes: 138 additions & 0 deletions Tests/LibraryTests/Utilities/ISOCreateTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: MIT
using System;
using System.IO;
using DiscUtils.Iso9660;
using DiscUtils.Udf;
using Xunit;

namespace LibraryTests.Utilities;

[Collection("ISOCreate command line")]
public class ISOCreateTest
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DefaultBuildsIsoWithExistingDirectoryBehavior(bool trailingSeparator)
{
using var tree = new SourceTree();
// The filename extension must not select UDF mode.
var output = Path.Combine(tree.Root, "default.udf");
new TestProgram().Execute("-q", "-vl", "ISO_TEST", output, tree.SourcePath(trailingSeparator));

using var image = File.OpenRead(output);
Assert.True(CDReader.Detect(image));
Assert.False(UdfReader.Detect(image));
using var isoReader = new CDReader(image, false);
Assert.Equal("ISO_TEST", isoReader.VolumeLabel);
using var reader = new CDReader(image, true);
Assert.False(reader.HasBootImage);
Assert.Equal("hello", Helpers.Helpers.GetFileContent("hello.txt", reader));
Assert.Equal("nested", Helpers.Helpers.GetFileContent("folder\\child.txt", reader));
Assert.Equal("unicode", Helpers.Helpers.GetFileContent("字幕\\繁體中文.txt", reader));
Assert.Equal(0, reader.GetFileLength("zero.txt"));
Assert.False(reader.DirectoryExists("empty"));
Assert.False(reader.DirectoryExists("folder\\empty"));
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void UdfSwitchPreservesFilesNamesAndEmptyDirectories(bool trailingSeparator)
{
using var tree = new SourceTree();
var output = Path.Combine(tree.Root, "udf.iso");
new TestProgram().Execute("-q", "-udf", "-vollabel", "UDF_TEST", output, tree.SourcePath(trailingSeparator));

using var image = File.OpenRead(output);
Assert.True(UdfReader.Detect(image));
Assert.False(CDReader.Detect(image));
using var reader = new UdfReader(image);
Assert.Equal("UDF_TEST", reader.VolumeLabel);
Assert.Equal("hello", Helpers.Helpers.GetFileContent("hello.txt", reader));
Assert.Equal("nested", Helpers.Helpers.GetFileContent("folder\\child.txt", reader));
Assert.Equal("unicode", Helpers.Helpers.GetFileContent("字幕\\繁體中文.txt", reader));
Assert.Equal(0, reader.GetFileLength("zero.txt"));
Assert.True(reader.DirectoryExists("empty"));
Assert.True(reader.DirectoryExists("folder\\empty"));
}

[Fact]
public void UdfSwitchWorksWithoutVolumeLabel()
{
using var tree = new SourceTree();
var output = Path.Combine(tree.Root, "udf.iso");
new TestProgram().Execute("-q", output, tree.Source, "-udf");

using var image = File.OpenRead(output);
using var reader = new UdfReader(image);
Assert.Equal("DISC", reader.VolumeLabel);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void UdfBootImageFailsBeforeOpeningOrOverwritingOutput(bool existingOutput)
{
using var tree = new SourceTree();
var output = Path.Combine(tree.Root, "udf.iso");
var original = new byte[] { 1, 2, 3 };
if (existingOutput)
{
File.WriteAllBytes(output, original);
}

var previousExitCode = Environment.ExitCode;
try
{
Environment.ExitCode = 0;
new TestProgram().Execute("-q", "-udf", output, tree.Source, Path.Combine(tree.Root, "missing-boot.img"));
Assert.Equal(1, Environment.ExitCode);
if (existingOutput)
{
Assert.Equal(original, File.ReadAllBytes(output));
}
else
{
Assert.False(File.Exists(output));
}
}
finally
{
Environment.ExitCode = previousExitCode;
}
}

private sealed class TestProgram : ISOCreate.Program
{
public void Execute(params string[] args) => Run(args);
}

private sealed class SourceTree : IDisposable
{
public string Root { get; } = Path.Combine(Path.GetTempPath(), "isocreate-" + Guid.NewGuid().ToString("N"));
public string Source { get; }

public SourceTree()
{
Source = Path.Combine(Root, "source");
Directory.CreateDirectory(Path.Combine(Source, "empty"));
Directory.CreateDirectory(Path.Combine(Source, "folder", "empty"));
Directory.CreateDirectory(Path.Combine(Source, "字幕"));
File.WriteAllText(Path.Combine(Source, "hello.txt"), "hello");
File.WriteAllText(Path.Combine(Source, "folder", "child.txt"), "nested");
File.WriteAllText(Path.Combine(Source, "字幕", "繁體中文.txt"), "unicode");
File.WriteAllBytes(Path.Combine(Source, "zero.txt"), new byte[0]);
}

public string SourcePath(bool trailingSeparator) => trailingSeparator ? Source + Path.DirectorySeparatorChar : Source;

public void Dispose() => Directory.Delete(Root, recursive: true);
}
}

// These tests temporarily set the process exit code to exercise command-line failures.
[CollectionDefinition("ISOCreate command line", DisableParallelization = true)]
public class ISOCreateCollection
{
}
2 changes: 2 additions & 0 deletions Utilities/ISOCreate/ISOCreate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<ProjectReference Include="..\DiscUtils.Common\DiscUtils.Common.csproj" />
<ProjectReference Include="..\..\Library\DiscUtils.Core\DiscUtils.Core.csproj" />
<ProjectReference Include="..\..\Library\DiscUtils.Iso9660\DiscUtils.Iso9660.csproj" />
<ProjectReference Include="..\..\Library\DiscUtils.Udf\DiscUtils.Udf.csproj" />
<InternalsVisibleTo Include="LibraryTests" />
</ItemGroup>

</Project>
49 changes: 38 additions & 11 deletions Utilities/ISOCreate/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@
using System.IO;
using DiscUtils.Common;
using DiscUtils.Iso9660;
using DiscUtils.Udf;

namespace ISOCreate;

class Program : ProgramBase
{
private CommandLineParameter _isoFileParam;
private CommandLineParameter _imageFileParam;
private CommandLineParameter _srcDir;
private CommandLineParameter _bootImage;
private CommandLineSwitch _volLabelSwitch;
private CommandLineSwitch _udfSwitch;

static void Main(params string[] args)
{
Expand All @@ -42,15 +44,17 @@ static void Main(params string[] args)

protected override StandardSwitches DefineCommandLine(CommandLineParser parser)
{
_isoFileParam = new CommandLineParameter("iso_file", "The ISO file to create.", false);
_srcDir = new CommandLineParameter("sourcedir", "The directory to be added to the ISO", false);
_bootImage = new CommandLineParameter("bootimage", "The bootable disk image, to create a bootable ISO", true);
_volLabelSwitch = new CommandLineSwitch("vl", "vollabel", "label", "Volume Label for the ISO file.");
_imageFileParam = new CommandLineParameter("image_file", "The ISO or UDF image file to create.", false);
_srcDir = new CommandLineParameter("sourcedir", "The directory to be added to the image.", false);
_bootImage = new CommandLineParameter("bootimage", "The bootable disk image, to create a bootable ISO (unavailable with -udf).", true);
_volLabelSwitch = new CommandLineSwitch("vl", "vollabel", "label", "Volume label for the image file.");
_udfSwitch = new CommandLineSwitch("udf", null, "Create a UDF 2.01 image instead of the default ISO 9660 image.");

parser.AddParameter(_isoFileParam);
parser.AddParameter(_imageFileParam);
parser.AddParameter(_srcDir);
parser.AddParameter(_bootImage);
parser.AddSwitch(_volLabelSwitch);
parser.AddSwitch(_udfSwitch);

return StandardSwitches.Default;
}
Expand All @@ -64,6 +68,26 @@ protected override void DoRun()
Environment.Exit(1);
}

if (_udfSwitch.IsPresent)
{
if (_bootImage.IsPresent)
{
Console.Error.WriteLine("The bootimage parameter cannot be used with -udf. Boot images are supported only for ISO 9660 images.");
Environment.ExitCode = 1;
return;
}

var udfBuilder = new UdfBuilder();
if (_volLabelSwitch.IsPresent)
{
udfBuilder.VolumeIdentifier = _volLabelSwitch.Value;
}

PopulateFromFolder(di, di.FullName, udfBuilder.AddFile, udfBuilder.AddDirectory);
udfBuilder.Build(_imageFileParam.Value);
return;
}

var builder = new CDBuilder();

if (_volLabelSwitch.IsPresent)
Expand All @@ -76,21 +100,24 @@ protected override void DoRun()
builder.SetBootImage(new FileStream(_bootImage.Value, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 2 << 20, useAsync: false), BootDeviceEmulation.NoEmulation, 0);
}

PopulateFromFolder(builder, di, di.FullName);
PopulateFromFolder(di, di.FullName, (name, sourcePath) => builder.AddFile(name, sourcePath));

builder.Build(_isoFileParam.Value);
builder.Build(_imageFileParam.Value);
}

private static void PopulateFromFolder(CDBuilder builder, DirectoryInfo di, string basePath)
private static void PopulateFromFolder(DirectoryInfo di, string basePath, Action<string, string> addFile, Action<string> addDirectory = null)
{
foreach (var file in di.GetFiles())
{
builder.AddFile(file.FullName[basePath.Length..], file.FullName);
var name = file.FullName[basePath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
addFile(name, file.FullName);
}

foreach (var dir in di.GetDirectories())
{
PopulateFromFolder(builder, dir, basePath);
var name = dir.FullName[basePath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
addDirectory?.Invoke(name);
PopulateFromFolder(dir, basePath, addFile, addDirectory);
}
}
}
29 changes: 29 additions & 0 deletions Utilities/ISOCreate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# ISOCreate

Create an optical disc image from a source directory:

```text
ISOCreate [options] image_file sourcedir [bootimage]
```

The default is ISO 9660 with Joliet, using the existing ISO builder. Add `-udf`
to create a UDF 2.01 image. The output filename extension does not select the
format.

```text
ISOCreate -vl MY_DISC output.iso source
ISOCreate -udf -vl MY_DISC output.iso source
```

`-vl label` (or `-vollabel label`) sets the volume label for either format.
UDF labels use the builder's 31-byte encoded label limit (including the CS0
compression identifier); the default UDF label is `DISC`.

UDF mode includes files, subdirectories and empty directories. It writes the
complete image and file contents to the output file.

The optional `bootimage` parameter creates an El Torito bootable ISO, as before.
Combining it with `-udf` reports an error and exits with code 1 before writing the
output image. UDF mode does not create a bootable or hybrid ISO/UDF image.

Use `ISOCreate -help` to see all options.
Loading