Skip to content

Commit 22d083c

Browse files
committed
ISO-9660: Parse headers and critical volume descriptors
1 parent 10324cb commit 22d083c

15 files changed

+933
-3
lines changed
1.33 MB
Binary file not shown.

TotalImage.IO/FileSystems/FileSystem.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Immutable;
22
using System.IO;
33
using TotalImage.FileSystems.FAT;
4+
using TotalImage.FileSystems.ISO;
45

56
namespace TotalImage.FileSystems
67
{
@@ -10,7 +11,8 @@ namespace TotalImage.FileSystems
1011
public abstract class FileSystem
1112
{
1213
private readonly static ImmutableArray<IFileSystemFactory> _knownFactories = ImmutableArray.Create<IFileSystemFactory>(
13-
new FatFactory()
14+
new FatFactory(),
15+
new IsoFactory()
1416
);
1517

1618
/// <summary>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Collections.Immutable;
2+
using System.IO;
3+
using System.Linq;
4+
5+
namespace TotalImage.FileSystems.ISO
6+
{
7+
/*
8+
* The ISO 9660 implementation here is based on the ECMA-119 specification, 4th edition
9+
* https://www.ecma-international.org/wp-content/uploads/ECMA-119_4th_edition_june_2019.pdf
10+
* A copy has also been included in the TotalImage repository, under the docs folder
11+
*/
12+
13+
/// <summary>
14+
/// Representation of an ISO 9660 file system
15+
/// </summary>
16+
public class Iso9660FileSystem : FileSystem
17+
{
18+
/// <summary>
19+
/// The volume descriptors for the ISO 9660 file system
20+
/// </summary>
21+
public ImmutableArray<IsoVolumeDescriptor> VolumeDescriptors { get; }
22+
23+
/// <summary>
24+
/// The first primary volume descriptor
25+
/// </summary>
26+
public IsoPrimaryVolumeDescriptor? PrimaryVolumeDescriptor
27+
=> VolumeDescriptors
28+
.OfType<IsoPrimaryVolumeDescriptor>()
29+
.FirstOrDefault();
30+
31+
/// <summary>
32+
/// Create an ISO 9660 file system
33+
/// </summary>
34+
/// <param name="containerStream">The underlying stream</param>
35+
public Iso9660FileSystem(Stream containerStream) : base(containerStream)
36+
{
37+
containerStream.Seek(0x8000, SeekOrigin.Begin);
38+
39+
var volumeDescriptors = ImmutableArray.CreateBuilder<IsoVolumeDescriptor>();
40+
41+
byte[] recordBytes = new byte[2048];
42+
do
43+
{
44+
containerStream.Read(recordBytes);
45+
46+
var record = IsoVolumeDescriptor.ReadVolumeDescriptor(recordBytes);
47+
if (!record.IsValid())
48+
{
49+
throw new InvalidDataException();
50+
}
51+
52+
volumeDescriptors.Add(record);
53+
}
54+
while (volumeDescriptors[^1].Type != IsoVolumeDescriptorType.VolumeDescriptorSetTerminator);
55+
56+
VolumeDescriptors = volumeDescriptors.ToImmutable();
57+
58+
IsoPrimaryVolumeDescriptor? primaryDescriptor = PrimaryVolumeDescriptor;
59+
if (primaryDescriptor == null)
60+
{
61+
throw new InvalidDataException("No primary volume descriptor");
62+
}
63+
64+
VolumeLabel = primaryDescriptor.VolumeSetIdentifier;
65+
RootDirectory = primaryDescriptor.RootDirectory;
66+
TotalSize = primaryDescriptor.LogicalBlockSize * primaryDescriptor.VolumeSpace;
67+
}
68+
69+
/// <inheritdoc />
70+
public override string DisplayName => "ISO 9660";
71+
72+
/// <inheritdoc />
73+
public override string VolumeLabel { get; set; }
74+
75+
/// <inheritdoc />
76+
public override Directory RootDirectory { get; }
77+
78+
/// <inheritdoc />
79+
public override long TotalFreeSpace => 0;
80+
81+
/// <inheritdoc />
82+
public override long TotalSize { get; }
83+
}
84+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Text;
4+
5+
namespace TotalImage.FileSystems.ISO
6+
{
7+
/// <summary>
8+
/// Represents an ISO 9660 Boot Record
9+
/// </summary>
10+
public class IsoBootVolumeDescriptor : IsoVolumeDescriptor
11+
{
12+
/// <summary>
13+
/// An identifier of which system can read the boot record
14+
/// </summary>
15+
public string BootSystemIdentifier { get; }
16+
17+
/// <summary>
18+
/// An identifier for the boot record
19+
/// </summary>
20+
public string BootIdentifier { get; }
21+
22+
/// <summary>
23+
/// The raw binary to be used by the boot system
24+
/// </summary>
25+
public ImmutableArray<byte> BootSystemContent { get; }
26+
27+
/// <summary>
28+
/// Create an ISO 9660 Boot Record
29+
/// </summary>
30+
/// <param name="record">A span containing the volume descriptor record</param>
31+
/// <param name="type">The type of the volume descriptor</param>
32+
/// <param name="identifier">The volume descriptor identifier</param>
33+
/// <param name="version">The version of the volume descriptor</param>
34+
public IsoBootVolumeDescriptor(in ReadOnlySpan<byte> record, in IsoVolumeDescriptorType type, in ImmutableArray<byte> identifier, in byte version)
35+
: base(type, identifier, version)
36+
{
37+
Span<char> tmp = new char[32];
38+
39+
Encoding.ASCII.GetChars(record[7..39], tmp);
40+
BootSystemIdentifier = tmp.TrimEnd('\0').ToString();
41+
42+
Encoding.ASCII.GetChars(record[39..71], tmp);
43+
BootIdentifier = tmp.TrimEnd('\0').ToString();
44+
45+
BootSystemContent = record[71..].ToArray().ToImmutableArray();
46+
}
47+
}
48+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.IO;
5+
using System.Text;
6+
7+
namespace TotalImage.FileSystems.ISO
8+
{
9+
/// <summary>
10+
/// Represents a directory record within an ISO 9660 file system
11+
/// </summary>
12+
public class IsoDirectoryRecord : Directory
13+
{
14+
/// <summary>
15+
/// Used to separate a file name and extension in a file identifier
16+
/// </summary>
17+
public const char IDENTIFIER_SEPARATOR_1 = '.';
18+
19+
/// <summary>
20+
/// Used to separate a file extension and file version in a file identifier
21+
/// </summary>
22+
public const char IDENTIFIER_SEPARATOR_2 = ';';
23+
24+
#region Implemented Properties / Methods
25+
/// <inheritdoc />
26+
public override string Name { get => FileIdentifier; set => throw new NotImplementedException(); }
27+
28+
/// <inheritdoc />
29+
public override FileAttributes Attributes { get => 0; set => throw new NotImplementedException(); }
30+
31+
/// <inheritdoc />
32+
public override DateTime? LastAccessTime { get => null; set => throw new NotImplementedException(); }
33+
34+
/// <inheritdoc />
35+
public override DateTime? LastWriteTime { get => RecordingDate?.LocalDateTime.ToLocalTime(); set => throw new NotImplementedException(); }
36+
37+
/// <inheritdoc />
38+
public override DateTime? CreationTime { get => null; set => throw new NotImplementedException(); }
39+
40+
/// <inheritdoc />
41+
public override ulong Length { get => DataLength; set => throw new NotImplementedException(); }
42+
43+
public override Directory CreateSubdirectory(string path)
44+
{
45+
throw new NotImplementedException();
46+
}
47+
48+
public override void Delete()
49+
{
50+
throw new NotImplementedException();
51+
}
52+
53+
public override IEnumerable<FileSystemObject> EnumerateFileSystemObjects(bool showHidden, bool showDeleted)
54+
{
55+
return Array.Empty<FileSystemObject>();
56+
}
57+
58+
public override void MoveTo(string path)
59+
{
60+
throw new NotImplementedException();
61+
}
62+
#endregion
63+
64+
/// <summary>
65+
/// Length of directory record
66+
/// </summary>
67+
public byte RecordLength { get; }
68+
69+
/// <summary>
70+
/// Length of extended attribute record
71+
/// </summary>
72+
public byte ExtendedAttributeLength { get; }
73+
74+
/// <summary>
75+
/// Offset of extent
76+
/// </summary>
77+
public uint ExtentOffset { get; }
78+
79+
/// <summary>
80+
/// Length of data
81+
/// </summary>
82+
public uint DataLength { get; }
83+
84+
/// <summary>
85+
/// Time directory was recorded to disc
86+
/// </summary>
87+
public DateTimeOffset? RecordingDate { get; }
88+
89+
/// <summary>
90+
/// Flags indicating the characteristics of the directory
91+
/// </summary>
92+
public IsoFileFlags FileFlags { get; }
93+
94+
/// <summary>
95+
/// File unit size if data is recorded in interleave mode
96+
/// </summary>
97+
public byte FileUnitSize { get; }
98+
99+
/// <summary>
100+
/// The interleave gap size if data is recorded in interleave mode
101+
/// </summary>
102+
public byte InterleaveGapSize { get; }
103+
104+
/// <summary>
105+
/// The ordinal number of the volume that this extent appears on within the volume set
106+
/// </summary>
107+
public ushort VolumeSequenceNumber { get; }
108+
109+
/// <summary>
110+
/// Length of the file identifier
111+
/// </summary>
112+
public byte FileIdentifierLength { get; }
113+
114+
/// <summary>
115+
/// The identifier of the file
116+
/// </summary>
117+
public string FileIdentifier { get; }
118+
119+
/// <summary>
120+
/// Raw binary data reserved for system use
121+
/// </summary>
122+
public ImmutableArray<byte> SystemUseContent { get; }
123+
124+
/// <summary>
125+
/// Create an ISO 9660 directory record
126+
/// </summary>
127+
/// <param name="record">A span containing the directory record</param>
128+
/// <exception cref="ArgumentOutOfRangeException">Thrown if the span provided does not cover the entire record</exception>
129+
public IsoDirectoryRecord(in ReadOnlySpan<byte> record) : base(null, null)
130+
{
131+
if (record[0] > record.Length)
132+
{
133+
throw new ArgumentOutOfRangeException(nameof(record));
134+
}
135+
136+
RecordLength = record[0];
137+
ExtendedAttributeLength = record[1];
138+
ExtentOffset = IsoUtilities.ReadUInt32MultiEndian(record[2..10]);
139+
DataLength = IsoUtilities.ReadUInt32MultiEndian(record[10..18]);
140+
RecordingDate = IsoUtilities.FromIsoRecordingDateTime(record[18..25]);
141+
FileFlags = (IsoFileFlags)record[25];
142+
FileUnitSize = record[26];
143+
InterleaveGapSize = record[27];
144+
VolumeSequenceNumber = IsoUtilities.ReadUInt16MultiEndian(record[28..32]);
145+
FileIdentifierLength = record[32];
146+
147+
char[] textBuffer = new char[FileIdentifierLength];
148+
Encoding.ASCII.GetChars(record[33..(33 + FileIdentifierLength)], textBuffer);
149+
FileIdentifier = textBuffer.AsSpan().Trim('\0').ToString();
150+
151+
int systemUseStart = 33 + FileIdentifierLength;
152+
if (systemUseStart % 2 == 1) systemUseStart++; // account for padding field
153+
154+
if (systemUseStart == RecordLength)
155+
{
156+
SystemUseContent = Array.Empty<byte>().ToImmutableArray();
157+
}
158+
else
159+
{
160+
record[systemUseStart..RecordLength].ToArray().ToImmutableArray();
161+
}
162+
}
163+
}
164+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
5+
namespace TotalImage.FileSystems.ISO
6+
{
7+
/// <summary>
8+
/// A factory class that can create an ISO 9660 file system
9+
/// </summary>
10+
public class IsoFactory : IFileSystemFactory
11+
{
12+
/// <inheritdoc />
13+
public FileSystem? TryLoadFileSystem(Stream stream)
14+
{
15+
stream.Seek(0x8001, SeekOrigin.Begin);
16+
byte[] identifier = new byte[5];
17+
stream.Read(identifier);
18+
if (identifier.SequenceEqual(IsoVolumeDescriptor.StandardIdentifier))
19+
{
20+
return new Iso9660FileSystem(stream);
21+
}
22+
23+
return null;
24+
}
25+
}
26+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
3+
namespace TotalImage.FileSystems.ISO
4+
{
5+
/// <summary>
6+
/// Flags set for a file on the ISO 9660 file system
7+
/// </summary>
8+
[Flags]
9+
public enum IsoFileFlags : byte
10+
{
11+
/// <summary>
12+
/// If set, the existence of the file should not be shown to the user
13+
/// </summary>
14+
Existence = 1 << 0,
15+
16+
/// <summary>
17+
/// If set, the record is a directory
18+
/// </summary>
19+
Directory = 1 << 1,
20+
21+
/// <summary>
22+
/// If set, the record is an associated file
23+
/// </summary>
24+
AssociatedFile = 1 << 2,
25+
26+
/// <summary>
27+
/// If set, the record is an extended attribute record
28+
/// </summary>
29+
Record = 1 << 3,
30+
31+
/// <summary>
32+
/// If set, permissions for this file are set in an extended attribute record
33+
/// </summary>
34+
Protection = 1 << 4,
35+
36+
/// <summary>
37+
/// If set, this is not the final record for the entry
38+
/// </summary>
39+
MultiExtent = 1 << 7
40+
}
41+
}

0 commit comments

Comments
 (0)