Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
97 changes: 97 additions & 0 deletions Compression.Tests/Nrg/NrgAbsoluteMultisessionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Buffers.Binary;
using FileFormat.Nrg;

namespace Compression.Tests.Nrg;

[TestFixture]
public class NrgAbsoluteMultisessionTests {
private const int IsoSectorSize = 2048;

[Test, Category("Regression")]
public void LaterSession_AbsoluteIsoExtents_AreResolvedAgainstTrackIndexOne() {
var image = Write(new NrgDiscDefinition([
new NrgSessionDefinition([
new NrgTrackDefinition(NrgTrackMode.Mode1, BuildIso("FIRST.BIN", "first"u8.ToArray())),
]),
new NrgSessionDefinition([
new NrgTrackDefinition(NrgTrackMode.Mode1, BuildIso("SECOND.BIN", "second-session"u8.ToArray())),
]),
]));

NrgTrackInfo secondTrack;
using (var initialStream = new MemoryStream(image, writable: false))
using (var initialReader = new NrgReader(initialStream)) {
secondTrack = initialReader.Tracks.Single(static track => track.SessionNumber == 2);
Assert.That(secondTrack.Index1Lba, Is.GreaterThan(0));
}

RewriteSecondIsoToAbsoluteExtents(image, secondTrack);

using var stream = new MemoryStream(image, writable: false);
using var reader = new NrgReader(stream);
var entry = reader.Entries.Single(entry =>
entry.SessionNumber == 2 && entry.Name.Equals("SECOND.BIN", StringComparison.OrdinalIgnoreCase));

Assert.Multiple(() => {
Assert.That(entry.FullPath, Is.EqualTo(".nrg/session-02/track-02/iso/SECOND.BIN").IgnoreCase);
Assert.That(entry.StartLba, Is.GreaterThan(secondTrack.Index1Lba!.Value));
Assert.That(reader.Extract(entry), Is.EqualTo("second-session"u8.ToArray()));
});
}

private static void RewriteSecondIsoToAbsoluteExtents(byte[] image, NrgTrackInfo track) {
var baseLba = track.Index1Lba ?? throw new AssertionException("Second session has no index-1 LBA.");
if (baseLba <= 0)
throw new AssertionException("Second session index-1 LBA must be positive.");

var pvdOffset = checked((int)(track.DataOffset + 16L * track.SectorSize + track.UserDataOffset));
var pvd = image.AsSpan(pvdOffset, IsoSectorSize);
Assert.That(pvd[0], Is.EqualTo(1));
Assert.That(pvd.Slice(1, 5).SequenceEqual("CD001"u8), Is.True);

var relativeRootLba = BinaryPrimitives.ReadUInt32LittleEndian(pvd.Slice(158, 4));
var rootSize = BinaryPrimitives.ReadUInt32LittleEndian(pvd.Slice(166, 4));
WriteBothEndianExtent(pvd.Slice(158, 8), checked(relativeRootLba + (uint)baseLba));

var consumed = 0L;
while (consumed < rootSize) {
var sectorIndex = consumed / IsoSectorSize;
var sectorOffset = checked((int)(
track.DataOffset + ((long)relativeRootLba + sectorIndex) * track.SectorSize + track.UserDataOffset));
var sector = image.AsSpan(sectorOffset, IsoSectorSize);
var withinSector = 0;

while (withinSector < IsoSectorSize && consumed + withinSector < rootSize) {
var recordLength = sector[withinSector];
if (recordLength == 0)
break;
if (recordLength < 34 || withinSector + recordLength > IsoSectorSize)
throw new AssertionException("Generated ISO contains an invalid root-directory record.");

var record = sector.Slice(withinSector, recordLength);
var relativeExtent = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(2, 4));
WriteBothEndianExtent(record.Slice(2, 8), checked(relativeExtent + (uint)baseLba));
withinSector += recordLength;
}

consumed += IsoSectorSize;
}
}

private static void WriteBothEndianExtent(Span<byte> destination, uint value) {
BinaryPrimitives.WriteUInt32LittleEndian(destination[..4], value);
BinaryPrimitives.WriteUInt32BigEndian(destination[4..8], value);
}

private static byte[] BuildIso(string name, byte[] data) {
var writer = new FileSystem.Iso.IsoWriter();
writer.AddFile(name, data);
return writer.Build();
}

private static byte[] Write(NrgDiscDefinition disc) {
using var output = new MemoryStream();
NrgWriter.Write(output, disc);
return output.ToArray();
}
}
216 changes: 216 additions & 0 deletions Compression.Tests/Nrg/NrgLegacyDescriptorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
using System.Buffers.Binary;
using FileFormat.Nrg;

namespace Compression.Tests.Nrg;

[TestFixture]
public class NrgLegacyDescriptorTests {
private const int SectorSize = 2048;

[Test, Category("Regression")]
public void Version1_CuesAndDaoi_DecodeMsfAddresses() {
var payload = "legacy-v1"u8.ToArray();
var iso = BuildIso("LEGACY.BIN", payload);
var sectors = iso.Length / SectorSize;
var image = BuildLegacyDaoImage(
[iso],
[new SessionVector(1, -150, 0, sectors)],
reorderDescriptorChunks: false);

using var stream = new MemoryStream(image, writable: false);
using var reader = new NrgReader(stream);
var session = reader.Sessions.Single();
var track = reader.Tracks.Single();
var entry = reader.Entries.Single(entry =>
entry.Kind == NrgEntryKind.IsoFile && entry.Name.Equals("LEGACY.BIN", StringComparison.OrdinalIgnoreCase));

Assert.Multiple(() => {
Assert.That(reader.Version, Is.EqualTo(1));
Assert.That(session.LeadInLba, Is.EqualTo(-150));
Assert.That(session.LeadOutLba, Is.EqualTo(sectors));
Assert.That(track.TrackNumber, Is.EqualTo(1));
Assert.That(track.Index0Lba, Is.Zero);
Assert.That(track.Index1Lba, Is.Zero);
Assert.That(track.HasIso9660, Is.True);
Assert.That(reader.Extract(entry), Is.EqualTo(payload));
});
}

[Test, Category("Regression")]
public void Version1_ReorderedCueAndDaoChunks_AreCorrelatedByOrdinal() {
var firstPayload = "first-v1-session"u8.ToArray();
var secondPayload = "second-v1-session"u8.ToArray();
var firstIso = BuildIso("FIRST.BIN", firstPayload);
var secondIso = BuildIso("SECOND.BIN", secondPayload);
var firstSectors = firstIso.Length / SectorSize;
var secondStartLba = 12_000;
var secondSectors = secondIso.Length / SectorSize;

var image = BuildLegacyDaoImage(
[firstIso, secondIso],
[
new SessionVector(1, -150, 0, firstSectors),
new SessionVector(2, 7_500, secondStartLba, secondStartLba + secondSectors),
],
reorderDescriptorChunks: true);

using var stream = new MemoryStream(image, writable: false);
using var reader = new NrgReader(stream);

Assert.Multiple(() => {
Assert.That(reader.Version, Is.EqualTo(1));
Assert.That(reader.Sessions, Has.Count.EqualTo(2));
Assert.That(reader.Tracks, Has.Count.EqualTo(2));
Assert.That(reader.Sessions[0].LeadInLba, Is.EqualTo(-150));
Assert.That(reader.Sessions[1].LeadInLba, Is.EqualTo(7_500));
Assert.That(reader.Tracks[0].TrackNumber, Is.EqualTo(1));
Assert.That(reader.Tracks[0].Index1Lba, Is.Zero);
Assert.That(reader.Tracks[1].TrackNumber, Is.EqualTo(2));
Assert.That(reader.Tracks[1].Index1Lba, Is.EqualTo(secondStartLba));
Assert.That(reader.Tracks.All(static track => track.HasIso9660), Is.True);
});

var first = reader.Entries.Single(entry => entry.Name.Equals("FIRST.BIN", StringComparison.OrdinalIgnoreCase));
var second = reader.Entries.Single(entry => entry.Name.Equals("SECOND.BIN", StringComparison.OrdinalIgnoreCase));
Assert.Multiple(() => {
Assert.That(first.FullPath, Is.EqualTo("FIRST.BIN").IgnoreCase);
Assert.That(second.FullPath, Is.EqualTo(".nrg/session-02/track-02/iso/SECOND.BIN").IgnoreCase);
Assert.That(reader.Extract(first), Is.EqualTo(firstPayload));
Assert.That(reader.Extract(second), Is.EqualTo(secondPayload));
});
}

[Test, Category("Regression")]
public void Version2_Cuex_PreservesSignedLbas() {
var pregap = new byte[2 * 2352];
var audio = new byte[2352];
using var output = new MemoryStream();
NrgWriter.Write(output, new NrgDiscDefinition([
new NrgSessionDefinition([
new NrgTrackDefinition(NrgTrackMode.Audio, audio) {
PregapData = pregap,
},
]),
]));

using var stream = new MemoryStream(output.ToArray(), writable: false);
using var reader = new NrgReader(stream);
var track = reader.Tracks.Single();

Assert.Multiple(() => {
Assert.That(reader.Version, Is.EqualTo(2));
Assert.That(reader.Sessions.Single().LeadInLba, Is.EqualTo(-150));
Assert.That(track.Index0Lba, Is.EqualTo(-2));
Assert.That(track.Index1Lba, Is.Zero);
});
}

private static byte[] BuildLegacyDaoImage(
IReadOnlyList<byte[]> trackData,
IReadOnlyList<SessionVector> sessions,
bool reorderDescriptorChunks) {
if (trackData.Count != sessions.Count)
throw new ArgumentException("Each session vector must have one matching track payload.");

using var output = new MemoryStream();
var offsets = new (int Start, int End)[trackData.Count];
for (var i = 0; i < trackData.Count; ++i) {
var start = checked((int)output.Position);
output.Write(trackData[i]);
offsets[i] = (start, checked((int)output.Position));
}

var trailerOffset = checked((uint)output.Position);
var cuePayloads = sessions.Select(BuildCues).ToArray();
var daoPayloads = sessions.Select((session, index) => BuildDaoi(session.TrackNumber, offsets[index].Start, offsets[index].End)).ToArray();

if (reorderDescriptorChunks) {
foreach (var cue in cuePayloads)
WriteChunk(output, "CUES"u8, cue);
foreach (var dao in daoPayloads)
WriteChunk(output, "DAOI"u8, dao);
} else {
for (var i = 0; i < sessions.Count; ++i) {
WriteChunk(output, "CUES"u8, cuePayloads[i]);
WriteChunk(output, "DAOI"u8, daoPayloads[i]);
}
}

WriteChunk(output, "END!"u8, ReadOnlySpan<byte>.Empty);

Span<byte> footer = stackalloc byte[8];
"NERO"u8.CopyTo(footer);
BinaryPrimitives.WriteUInt32BigEndian(footer[4..], trailerOffset);
output.Write(footer);
return output.ToArray();
}

private static byte[] BuildCues(SessionVector session) {
var result = new byte[32];
WriteOldCue(result.AsSpan(0, 8), 0x41, 0, 0, session.LeadInLba);
WriteOldCue(result.AsSpan(8, 8), 0x41, session.TrackNumber, 0, session.Index1Lba);
WriteOldCue(result.AsSpan(16, 8), 0x41, session.TrackNumber, 1, session.Index1Lba);
WriteOldCue(result.AsSpan(24, 8), 0x41, -1, 1, session.LeadOutLba);
return result;
}

private static byte[] BuildDaoi(int trackNumber, int startOffset, int endOffset) {
var result = new byte[52];
BinaryPrimitives.WriteUInt32BigEndian(result, checked((uint)result.Length));
result[20] = checked((byte)trackNumber);
result[21] = checked((byte)trackNumber);

var track = result.AsSpan(22, 30);
BinaryPrimitives.WriteUInt16BigEndian(track[12..], checked((ushort)SectorSize));
track[14] = (byte)NrgTrackMode.Mode1;
BinaryPrimitives.WriteUInt16BigEndian(track[16..], 1);
BinaryPrimitives.WriteInt32BigEndian(track[18..], startOffset);
BinaryPrimitives.WriteInt32BigEndian(track[22..], startOffset);
BinaryPrimitives.WriteInt32BigEndian(track[26..], endOffset);
return result;
}

private static void WriteOldCue(Span<byte> destination, byte adrCtl, int trackNumber, int index, int lba) {
destination.Clear();
destination[0] = adrCtl;
destination[1] = trackNumber < 0 ? (byte)0xAA : ToBcd(trackNumber);
destination[2] = ToBcd(index);

var absoluteFrame = checked(lba + 150);
if (absoluteFrame < 0)
throw new ArgumentOutOfRangeException(nameof(lba));
var minute = absoluteFrame / (60 * 75);
var remainder = absoluteFrame % (60 * 75);
var second = remainder / 75;
var frame = remainder % 75;
if (minute > byte.MaxValue)
throw new ArgumentOutOfRangeException(nameof(lba));

destination[4] = 0;
destination[5] = checked((byte)minute);
destination[6] = checked((byte)second);
destination[7] = checked((byte)frame);
}

private static byte ToBcd(int value) {
if (value is < 0 or > 99)
throw new ArgumentOutOfRangeException(nameof(value));
return checked((byte)(((value / 10) << 4) | value % 10));
}

private static void WriteChunk(Stream output, ReadOnlySpan<byte> id, ReadOnlySpan<byte> payload) {
Span<byte> header = stackalloc byte[8];
id.CopyTo(header);
BinaryPrimitives.WriteUInt32BigEndian(header[4..], checked((uint)payload.Length));
output.Write(header);
output.Write(payload);
}

private static byte[] BuildIso(string name, byte[] data) {
var writer = new FileSystem.Iso.IsoWriter();
writer.AddFile(name, data);
return writer.Build();
}

private sealed record SessionVector(int TrackNumber, int LeadInLba, int Index1Lba, int LeadOutLba);
}
Loading
Loading