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 865fe55fc..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,14 @@ public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) } else { - _records.Add(childDirEntry); + if (_records.TryGetValue(childDirEntry.FileName, out var existingEntry)) + { + existingEntry._records.Add(dr); + } + else + { + _records.Add(childDirEntry); + } } } else if (dr.FileIdentifier == "\0") @@ -91,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; + } } }); 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 000000000..7794609f6 Binary files /dev/null and b/Tests/LibraryTests/Iso9660/Data/multiextent.iso_header.gz differ diff --git a/Tests/LibraryTests/Iso9660/SampleDataTests.cs b/Tests/LibraryTests/Iso9660/SampleDataTests.cs index 5a14502ca..403636380 100644 --- a/Tests/LibraryTests/Iso9660/SampleDataTests.cs +++ b/Tests/LibraryTests/Iso9660/SampleDataTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; using DiscUtils.Iso9660; using LibraryTests.Utilities; using Xunit; @@ -11,17 +12,40 @@ public class SampleDataTests [Fact] public void AppleTestZip() { - using var iso = Helpers.Helpers.LoadTestDataFileFromGZipFile("Iso9660", "apple-test.iso.gz"); + using var iso = Helpers.Helpers.LoadTestDataFileFromGZipFile(nameof(Iso9660), "apple-test.iso.gz"); using var cr = new CDReader(iso, false); var dir = cr.GetDirectoryInfo("sub-directory"); Assert.NotNull(dir); Assert.Equal("sub-directory", dir.Name); - var file = dir.GetFiles("apple-test.txt"); - Assert.Single(file); - Assert.Equal(21, file.First().Length); - Assert.Equal("apple-test.txt", file.First().Name); - Assert.Equal(dir, file.First().Directory); + var files = dir.GetFiles("apple-test.txt").ToList(); + Assert.Single(files); + Assert.Equal(21, files.First().Length); + Assert.Equal("apple-test.txt", files.First().Name); + Assert.Equal(dir, files.First().Directory); + } + + [Fact] + public void MultiExtentFiles() + { + using var iso = Helpers.Helpers.LoadTestDataFileFromGZipFile(nameof(Iso9660), "multiextent.iso_header.gz"); + using var cr = new CDReader(iso, joliet: true, hideVersions: true); + + const string pathToMultiextentFiles = @"\PS3_GAME\USRDIR\Resource\Common"; + var fsEntries = cr.GetFileSystemEntries(pathToMultiextentFiles).ToList(); + Assert.Equal(11, fsEntries.Count); + + var dir = cr.GetDirectoryInfo(pathToMultiextentFiles); + var files = dir.GetFiles().ToList(); + Assert.Equal(10, files.Count); + + var misc0 = files.First(f => 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