diff --git a/OpenRA.Mods.Common/FileSystem/ISO9660.cs b/OpenRA.Mods.Common/FileSystem/ISO9660.cs new file mode 100644 index 0000000000..d07c3e19bd --- /dev/null +++ b/OpenRA.Mods.Common/FileSystem/ISO9660.cs @@ -0,0 +1,221 @@ +#region Copyright & License Information +/* + * Copyright (c) The OpenRA Developers and Contributors + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Text; +using OpenRA.FileSystem; +using OpenRA.Primitives; +using FS = OpenRA.FileSystem.FileSystem; + +namespace OpenRA.Mods.Common.FileSystem +{ + public class ISO9660Loader : IPackageLoader + { + public sealed class ISO9660Package : IReadOnlyPackage + { + public readonly record struct Entry(uint Offset, uint Length); + + public string Name { get; } + public string VolumeName { get; } + + public IEnumerable Contents => index.Keys; + + readonly Dictionary index = []; + readonly Stream s; + + readonly record struct DirectoryRecord(string Name, uint Offset, uint Length, bool IsDirectory); + static bool TryReadDirectoryRecord(Stream s, out DirectoryRecord record, bool isJoliet = false) + { + var start = s.Position; + var recordLength = s.ReadUInt8(); + if (recordLength == 0) + { + s.Position = start; + record = default; + return false; + } + + s.Position++; + var location = s.ReadUInt32(); + s.Position += 4; + var length = s.ReadUInt32(); + s.Position += 11; + var flags = s.ReadUInt8(); + s.Position += 6; + + var identifierLength = s.ReadUInt8(); + var buffer = identifierLength < 128 ? stackalloc byte[identifierLength] : new byte[identifierLength]; + s.ReadBytes(buffer); + var identifier = (isJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII).GetString(buffer); + + s.Position = start + recordLength; + + record = new DirectoryRecord(identifier.Split(';')[0], 2048 * location, length, (flags & 2) != 0); + return true; + } + + void EnumerateDirectories(DirectoryRecord parent, string path = null, bool isJoliet = false) + { + var pos = s.Position; + s.Position = parent.Offset; + + // Skip . and .. records + TryReadDirectoryRecord(s, out _); + TryReadDirectoryRecord(s, out _); + while (s.Position < parent.Offset + parent.Length) + { + if (!TryReadDirectoryRecord(s, out var child, isJoliet)) + break; + + var childPath = path != null ? $"{path}/{child.Name}" : child.Name; + if (child.IsDirectory) + EnumerateDirectories(child, childPath, isJoliet); + else + index[childPath] = new Entry(child.Offset, child.Length); + } + + s.Position = pos; + } + + public ISO9660Package(Stream s, string filename) + { + Name = filename; + this.s = s; + + try + { + var complete = false; + + // Skip system area + s.Position = 32768; + + // Parse volume descriptors + while (!complete && s.Position < s.Length) + { + var start = s.Position; + + var vdType = s.ReadUInt8(); + var vdIdentifier = s.ReadASCII(5); + if (vdIdentifier != "CD001") + throw new InvalidDataException("Invalid volume descriptor"); + + switch (vdType) + { + // Terminator + case 0xFF: + complete = true; + break; + + // Primary volume descriptor + case 0x01: + { + s.Position = start + 40; + VolumeName = s.ReadASCII(32).Trim(); + + s.Position = start + 156; + TryReadDirectoryRecord(s, out var root); + EnumerateDirectories(root); + break; + } + + // Supplementary volume descriptor + case 0x02: + { + s.Position = start + 7; + var volumeFlags = s.ReadUInt8(); + s.Position = start + 88; + var escape = s.ReadASCII(32).Trim('\x00'); + + // Joliet extension + if (volumeFlags == 0 && escape is "%/@" or "%/C" or "%/E") + { + s.Position = start + 156; + TryReadDirectoryRecord(s, out var root, isJoliet: true); + EnumerateDirectories(root, isJoliet: true); + } + + break; + } + } + + s.Position = start + 2048; + } + + index.TrimExcess(); + } + catch + { + Dispose(); + throw; + } + } + + public Stream GetStream(string filename) + { + if (!index.TryGetValue(filename, out var entry)) + return null; + + return SegmentStream.CreateWithoutOwningStream(s, entry.Offset, (int)entry.Length); + } + + public IReadOnlyPackage OpenPackage(string filename, FS context) + { + var childStream = GetStream(filename); + if (childStream == null) + return null; + + if (context.TryParsePackage(childStream, filename, out var package)) + return package; + + childStream.Dispose(); + return null; + } + + public bool Contains(string filename) + { + return index.ContainsKey(filename); + } + + public IReadOnlyDictionary Index => new ReadOnlyDictionary(index); + + public void Dispose() + { + s.Dispose(); + } + } + + bool IPackageLoader.TryParsePackage(Stream s, string filename, FS context, out IReadOnlyPackage package) + { + if (s.Length < 34816) + { + package = null; + return false; + } + + // Check the volume descriptor + var pos = s.Position; + s.Position = 32769; + var identifier = s.ReadASCII(5); + s.Position = pos; + + if (identifier != "CD001") + { + package = null; + return false; + } + + package = new ISO9660Package(s, filename); + return true; + } + } +}