From 061b357325a0b68cfcf9982c1e7fe2bb758c05f9 Mon Sep 17 00:00:00 2001 From: 13xforever Date: Sun, 4 May 2025 10:31:35 +0500 Subject: [PATCH 1/3] skip extra extents in multiextent iso9660 file entries when enumerating --- Library/DiscUtils.Iso9660/ReaderDirectory.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Library/DiscUtils.Iso9660/ReaderDirectory.cs b/Library/DiscUtils.Iso9660/ReaderDirectory.cs index 865fe55fc..495de5ea5 100644 --- a/Library/DiscUtils.Iso9660/ReaderDirectory.cs +++ b/Library/DiscUtils.Iso9660/ReaderDirectory.cs @@ -73,7 +73,10 @@ public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) } else { - _records.Add(childDirEntry); + if (!_records.ContainsKey(childDirEntry.FileName)) + { + _records.Add(childDirEntry); + } } } else if (dr.FileIdentifier == "\0") From bc545a68a7c37f79f16973b2e784112e00cbe26a Mon Sep 17 00:00:00 2001 From: 13xforever Date: Sun, 4 May 2025 10:56:06 +0500 Subject: [PATCH 2/3] add basic extents support --- Library/DiscUtils.Iso9660/File.cs | 34 ++++++++++---- Library/DiscUtils.Iso9660/ReaderDirEntry.cs | 47 +++++++++++--------- Library/DiscUtils.Iso9660/ReaderDirectory.cs | 17 +++++-- Library/DiscUtils.Iso9660/VfsCDReader.cs | 28 ++++++------ 4 files changed, 78 insertions(+), 48 deletions(-) diff --git a/Library/DiscUtils.Iso9660/File.cs b/Library/DiscUtils.Iso9660/File.cs index ca32245d5..5eac14e86 100644 --- a/Library/DiscUtils.Iso9660/File.cs +++ b/Library/DiscUtils.Iso9660/File.cs @@ -26,6 +26,7 @@ using DiscUtils.Streams; using System.Collections; using System.Collections.Generic; +using System.Linq; using LTRData.Extensions.Buffers; namespace DiscUtils.Iso9660; @@ -41,7 +42,7 @@ public File(IsoContext context, ReaderDirEntry dirEntry) _dirEntry = dirEntry; } - public virtual byte[] SystemUseData => _dirEntry.Record.SystemUseData; + public virtual byte[] SystemUseData => _dirEntry.RecordExtents[0].SystemUseData; public UnixFileSystemInfo UnixFileInfo { @@ -54,8 +55,7 @@ public UnixFileSystemInfo UnixFileInfo var suspRecords = new SuspRecords(_context, SystemUseData); - var pfi = - suspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); + var pfi = suspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); if (pfi != null) { return new UnixFileSystemInfo @@ -101,19 +101,35 @@ public FileAttributes FileAttributes set => throw new NotSupportedException(); } - public long FileLength => _dirEntry.Record.DataLength; + public long FileLength => _dirEntry.RecordExtentsDataLength; public IBuffer FileContent { get { - var es = new ExtentStream(_context.DataStream, _dirEntry.Record.LocationOfExtent, - _dirEntry.Record.DataLength, _dirEntry.Record.FileUnitSize, _dirEntry.Record.InterleaveGapSize); - return new StreamBuffer(es, Ownership.Dispose); + if (_dirEntry.RecordExtents is [var extent]) + { + return new StreamBuffer( + new ExtentStream(_context.DataStream, extent.LocationOfExtent, extent.DataLength, extent.FileUnitSize, extent.InterleaveGapSize), + Ownership.Dispose + ); + } + + return new StreamBuffer( + new ConcatStream( + Ownership.Dispose, + _dirEntry.RecordExtents.Select( + e => SparseStream.FromStream( + new ExtentStream(_context.DataStream, e.LocationOfExtent, e.DataLength, e.FileUnitSize, e.InterleaveGapSize), + Ownership.Dispose + ) + ) + ), + Ownership.Dispose + ); } } public IEnumerable EnumerateAllocationExtents() - => SingleValueEnumerable.Get(new StreamExtent(_dirEntry.Record.LocationOfExtent * IsoUtilities.SectorSize, - _dirEntry.Record.DataLength)); + => _dirEntry.RecordExtents.Select(e => new StreamExtent(e.LocationOfExtent * IsoUtilities.SectorSize, e.DataLength)); } diff --git a/Library/DiscUtils.Iso9660/ReaderDirEntry.cs b/Library/DiscUtils.Iso9660/ReaderDirEntry.cs index 2cda093f2..03e745074 100644 --- a/Library/DiscUtils.Iso9660/ReaderDirEntry.cs +++ b/Library/DiscUtils.Iso9660/ReaderDirEntry.cs @@ -22,7 +22,10 @@ using System; using System.Buffers; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.IO; +using System.Linq; using System.Text; using DiscUtils.Internal; using DiscUtils.Streams; @@ -35,19 +38,18 @@ internal sealed class ReaderDirEntry : VfsDirEntry { private readonly IsoContext _context; private readonly string _fileName; - private readonly DirectoryRecord _record; + internal readonly List _records = []; public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) { _context = context; - _record = dirRecord; - _fileName = _record.FileIdentifier; + _fileName = dirRecord.FileIdentifier; var rockRidge = !string.IsNullOrEmpty(_context.RockRidgeIdentifier); - if (context.SuspDetected && _record.SystemUseData != null) + if (context.SuspDetected && dirRecord.SystemUseData != null) { - SuspRecords = new SuspRecords(_context, _record.SystemUseData); + SuspRecords = new SuspRecords(_context, dirRecord.SystemUseData); } if (rockRidge && SuspRecords != null) @@ -55,7 +57,7 @@ public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) // The full name is taken from this record, even if it's a child-link record var nameEntries = SuspRecords.GetEntries(_context.RockRidgeIdentifier, "NM"); var rrName = new StringBuilder(); - if (nameEntries != null && nameEntries.Count > 0) + if (nameEntries?.Count > 0) { foreach (PosixNameSystemUseEntry nameEntry in nameEntries) { @@ -67,8 +69,7 @@ public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) // If this is a Rock Ridge child link, replace the dir record with that from the 'self' record // in the child directory. - var clEntry = - SuspRecords.GetEntry(_context.RockRidgeIdentifier, "CL"); + var clEntry = SuspRecords.GetEntry(_context.RockRidgeIdentifier, "CL"); if (clEntry != null) { _context.DataStream.Position = clEntry.ChildDirLocation * _context.VolumeDescriptor.LogicalBlockSize; @@ -76,13 +77,12 @@ public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) var firstSector = ArrayPool.Shared.Rent(_context.VolumeDescriptor.LogicalBlockSize); try { - _context.DataStream.ReadExactly(firstSector, 0, - _context.VolumeDescriptor.LogicalBlockSize); + _context.DataStream.ReadExactly(firstSector, 0, _context.VolumeDescriptor.LogicalBlockSize); - DirectoryRecord.ReadFrom(firstSector, _context.VolumeDescriptor.CharacterEncoding, out _record); - if (_record.SystemUseData != null) + DirectoryRecord.ReadFrom(firstSector, _context.VolumeDescriptor.CharacterEncoding, out dirRecord); + if (dirRecord.SystemUseData != null) { - SuspRecords = new SuspRecords(_context, _record.SystemUseData); + SuspRecords = new SuspRecords(_context, dirRecord.SystemUseData); } } finally @@ -92,9 +92,9 @@ public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) } } - LastAccessTimeUtc = _record.RecordingDateAndTime; - LastWriteTimeUtc = _record.RecordingDateAndTime; - CreationTimeUtc = _record.RecordingDateAndTime; + LastAccessTimeUtc =dirRecord.RecordingDateAndTime; + LastWriteTimeUtc = dirRecord.RecordingDateAndTime; + CreationTimeUtc = dirRecord.RecordingDateAndTime; if (rockRidge && SuspRecords != null) { @@ -119,6 +119,8 @@ public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) } } } + + _records.Add(dirRecord); } public override DateTime CreationTimeUtc { get; } @@ -147,12 +149,12 @@ public override FileAttributes FileAttributes attrs |= FileAttributes.ReadOnly; - if ((_record.Flags & FileFlags.Directory) != 0) + if ((_records[0].Flags & FileFlags.Directory) != 0) { attrs |= FileAttributes.Directory; } - if ((_record.Flags & FileFlags.Hidden) != 0) + if ((_records[0].Flags & FileFlags.Hidden) != 0) { attrs |= FileAttributes.Hidden; } @@ -167,7 +169,7 @@ public override FileAttributes FileAttributes public override bool HasVfsTimeInfo => true; - public override bool IsDirectory => (_record.Flags & FileFlags.Directory) != 0; + public override bool IsDirectory => (_records[0].Flags & FileFlags.Directory) != 0; public override bool IsSymlink => false; @@ -175,9 +177,12 @@ public override FileAttributes FileAttributes public override DateTime LastWriteTimeUtc { get; } - public DirectoryRecord Record => _record; + [Obsolete("Please use RecordExtents property instead.")] + public DirectoryRecord Record => _records[0]; + public ReadOnlyCollection RecordExtents => _records.AsReadOnly(); + public long RecordExtentsDataLength => _records.Sum(r => r.DataLength); public SuspRecords SuspRecords { get; } - public override long UniqueCacheId => ((long)_record.LocationOfExtent << 32) | _record.DataLength; + public override long UniqueCacheId => ((long)_records[0].LocationOfExtent << 32) | _records[0].DataLength; } \ No newline at end of file diff --git a/Library/DiscUtils.Iso9660/ReaderDirectory.cs b/Library/DiscUtils.Iso9660/ReaderDirectory.cs index 495de5ea5..d16380554 100644 --- a/Library/DiscUtils.Iso9660/ReaderDirectory.cs +++ b/Library/DiscUtils.Iso9660/ReaderDirectory.cs @@ -41,12 +41,17 @@ public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) var buffer = ArrayPool.Shared.Rent(IsoUtilities.SectorSize); try { + if (dirEntry.RecordExtents is not [var dirExtent]) + { + throw new NotSupportedException("MultiExtent directory entries are not supported"); + } + Array.Clear(buffer, 0, buffer.Length); - Stream extent = new ExtentStream(_context.DataStream, dirEntry.Record.LocationOfExtent, uint.MaxValue, 0, 0); + Stream extent = new ExtentStream(_context.DataStream, dirExtent.LocationOfExtent, uint.MaxValue, 0, 0); _records = new(StringComparer.OrdinalIgnoreCase, entry => entry.FileName); - var totalLength = dirEntry.Record.DataLength; + var totalLength = dirExtent.DataLength; uint totalRead = 0; while (totalRead < totalLength) { @@ -73,7 +78,11 @@ public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) } else { - if (!_records.ContainsKey(childDirEntry.FileName)) + if (_records.TryGetValue(childDirEntry.FileName, out var existingEntry)) + { + existingEntry._records.Add(dr); + } + else { _records.Add(childDirEntry); } @@ -94,7 +103,7 @@ public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) } } - public override byte[] SystemUseData => Self.Record.SystemUseData; + public override byte[] SystemUseData => Self.RecordExtents[0].SystemUseData; public IReadOnlyDictionary AllEntries => _records; diff --git a/Library/DiscUtils.Iso9660/VfsCDReader.cs b/Library/DiscUtils.Iso9660/VfsCDReader.cs index 2d5a50f5e..b41729546 100644 --- a/Library/DiscUtils.Iso9660/VfsCDReader.cs +++ b/Library/DiscUtils.Iso9660/VfsCDReader.cs @@ -305,14 +305,12 @@ public IEnumerable> PathToClusters(string path) var entry = GetDirectoryEntry(path) ?? throw new FileNotFoundException("File not found", path); - if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + if (entry.RecordExtents.Any(e => e.FileUnitSize != 0 || e.InterleaveGapSize != 0)) { throw new NotSupportedException("Non-contiguous extents not supported"); } - return SingleValueEnumerable.Get( - new Range(entry.Record.LocationOfExtent, - MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize))); + return entry.RecordExtents.Select(e => new Range(e.LocationOfExtent, MathUtilities.Ceil(e.DataLength, IsoUtilities.SectorSize))); } public IEnumerable PathToExtents(string path) @@ -320,13 +318,12 @@ public IEnumerable PathToExtents(string path) var entry = GetDirectoryEntry(path) ?? throw new FileNotFoundException("File not found", path); - if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + if (entry.RecordExtents.Any(e => e.FileUnitSize != 0 || e.InterleaveGapSize != 0)) { throw new NotSupportedException("Non-contiguous extents not supported"); } - return SingleValueEnumerable.Get( - new StreamExtent(entry.Record.LocationOfExtent * IsoUtilities.SectorSize, entry.Record.DataLength)); + return entry.RecordExtents.Select(e => new StreamExtent(e.LocationOfExtent * IsoUtilities.SectorSize, e.DataLength)); } public long GetAllocatedClustersCount(string path) @@ -334,12 +331,12 @@ public long GetAllocatedClustersCount(string path) var entry = GetDirectoryEntry(path) ?? throw new FileNotFoundException("File not found", path); - if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + if (entry.RecordExtents.Any(e => e.FileUnitSize != 0 || e.InterleaveGapSize != 0)) { throw new NotSupportedException("Non-contiguous extents not supported"); } - return entry.Record.DataLength / IsoUtilities.SectorSize; + return entry.RecordExtentsDataLength / IsoUtilities.SectorSize; } public ClusterMap BuildClusterMap() @@ -371,16 +368,19 @@ public ClusterMap BuildClusterMap() fileIdToPaths[entry.UniqueCacheId] = Array.AsReadOnly(newPaths); } - if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + if (entry.RecordExtents.Any(e => e.FileUnitSize != 0 || e.InterleaveGapSize != 0)) { throw new NotSupportedException("Non-contiguous extents not supported"); } - long clusters = MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize); - for (long i = 0; i < clusters; ++i) + foreach (var e in entry.RecordExtents) { - clusterToRole[i + entry.Record.LocationOfExtent] = ClusterRoles.DataFile; - clusterToFileId[i + entry.Record.LocationOfExtent] = entry.UniqueCacheId; + long clusters = MathUtilities.Ceil(e.DataLength, IsoUtilities.SectorSize); + for (long i = 0; i < clusters; ++i) + { + clusterToRole[i + e.LocationOfExtent] = ClusterRoles.DataFile; + clusterToFileId[i + e.LocationOfExtent] = entry.UniqueCacheId; + } } }); From 6616c8c4f9a3ca5ab8cd90f08012096dd78e6fe5 Mon Sep 17 00:00:00 2001 From: 13xforever Date: Sun, 4 May 2025 11:57:21 +0500 Subject: [PATCH 3/3] add basic test case for multiextent files --- .../Iso9660/Data/multiextent.iso_header.gz | Bin 0 -> 14748 bytes Tests/LibraryTests/Iso9660/SampleDataTests.cs | 36 +++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 Tests/LibraryTests/Iso9660/Data/multiextent.iso_header.gz diff --git a/Tests/LibraryTests/Iso9660/Data/multiextent.iso_header.gz b/Tests/LibraryTests/Iso9660/Data/multiextent.iso_header.gz new file mode 100644 index 0000000000000000000000000000000000000000..7794609f6d287212da4991ea119e5d4664857321 GIT binary patch literal 14748 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 z f.Name is "Misc0.FPK"); + var misc1 = files.First(f => f.Name is "Misc1.FPK"); + Assert.Equal(1464404972, misc0.Length); + Assert.Equal(1585521232, misc1.Length); + + var meClusters = cr.PathToClusters(misc0.FullName).ToList(); + Assert.Equal(2, meClusters.Count); } } \ No newline at end of file