using System; using System.IO; using SabreTools.Data.Models.CDROM; using SabreTools.IO.Extensions; using SabreTools.Numerics.Extensions; namespace SabreTools.Data.Extensions { public static class CDROMExtensions { /// /// Get the sector mode for a CD-ROM stream /// /// Stream to derive the sector mode from /// Sector mode from the stream on success, on error public static SectorMode GetSectorMode(this Stream stream) { try { byte modeByte = stream.ReadByteValue(); if (modeByte == 0) { return SectorMode.MODE0; } else if (modeByte == 1) { return SectorMode.MODE1; } else if (modeByte == 2) { stream.SeekIfPossible(2, SeekOrigin.Current); byte submode = stream.ReadByteValue(); if ((submode & 0x20) == 0x20) return SectorMode.MODE2_FORM2; else return SectorMode.MODE2_FORM1; } else { return SectorMode.UNKNOWN; } } catch { // Ignore the actual error return SectorMode.UNKNOWN; } } /// /// Get the user data size for a sector mode /// /// Sector mode to get a value for /// User data size, if possible public static long GetUserDataSize(this SectorMode mode) { return mode switch { SectorMode.UNKNOWN => Constants.Mode0DataSize, SectorMode.MODE0 => Constants.Mode0DataSize, SectorMode.MODE1 => Constants.Mode1DataSize, SectorMode.MODE2 => Constants.Mode0DataSize, SectorMode.MODE2_FORM1 => Constants.Mode2Form1DataSize, SectorMode.MODE2_FORM2 => Constants.Mode2Form2DataSize, _ => Constants.Mode0DataSize, }; } /// /// Get the user data end offset for a sector mode /// /// Sector mode to get a value for /// User data end offset, if possible public static long GetUserDataEnd(this SectorMode mode) { return mode switch { SectorMode.UNKNOWN => Constants.Mode0UserDataEnd, SectorMode.MODE0 => Constants.Mode0UserDataEnd, // TODO: Support flexible sector length (2352) SectorMode.MODE1 => Constants.Mode1UserDataEnd, SectorMode.MODE2 => Constants.Mode0UserDataEnd, // TODO: Support flexible sector length (2352) SectorMode.MODE2_FORM1 => Constants.Mode2Form1UserDataEnd, SectorMode.MODE2_FORM2 => Constants.Mode2Form2UserDataEnd, // TODO: Support flexible sector length (2348) _ => Constants.Mode0UserDataEnd, }; } /// /// Get the user data start offset for a sector mode /// /// Sector mode to get a value for /// User data start offset, if possible public static long GetUserDataStart(this SectorMode mode) { return mode switch { SectorMode.UNKNOWN => Constants.Mode0UserDataStart, SectorMode.MODE0 => Constants.Mode0UserDataStart, SectorMode.MODE1 => Constants.Mode1UserDataStart, SectorMode.MODE2 => Constants.Mode0UserDataStart, SectorMode.MODE2_FORM1 => Constants.Mode2Form1UserDataStart, SectorMode.MODE2_FORM2 => Constants.Mode2Form2UserDataStart, _ => Constants.Mode0UserDataStart, }; } /// /// Creates a stream that provides only the user data of a CDROM stream /// public class ISO9660Stream : Stream { // Base CDROM stream (2352-byte sector) private readonly Stream _baseStream; // State variables private long _position = 0; private SectorMode _currentMode = SectorMode.UNKNOWN; private long _userDataStart = Constants.Mode1UserDataStart; private long _userDataEnd = Constants.Mode1UserDataEnd; #pragma warning disable IDE0044 private long _isoSectorSize = Constants.Mode1DataSize; #pragma warning restore IDE0044 public ISO9660Stream(Stream inputStream) { if (!inputStream.CanSeek || !inputStream.CanRead) throw new ArgumentException("Stream must be readable and seekable.", nameof(inputStream)); _baseStream = inputStream; } /// public override bool CanRead => _baseStream.CanRead; /// public override bool CanSeek => _baseStream.CanSeek; /// public override bool CanWrite => false; /// public override void Flush() => _baseStream.Flush(); /// public override long Length => _baseStream.Length / Constants.CDROMSectorSize * _isoSectorSize; /// public override void SetLength(long value) => throw new NotSupportedException("Setting the length of this stream is not supported."); /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException("Writing to this stream is not supported."); /// protected override void Dispose(bool disposing) { if (disposing) _baseStream.Dispose(); base.Dispose(disposing); } /// public override long Position { // Get the position of the underlying ISO9660 stream get { // Get the user data location based on the current sector mode SetState(_position); // Get the number of ISO sectors before current position long isoPosition = _position / Constants.CDROMSectorSize * _isoSectorSize; // Add the within-sector position long remainder = _position % Constants.CDROMSectorSize; if (remainder > _userDataEnd) isoPosition += _isoSectorSize; else if (remainder > _userDataStart) isoPosition += remainder - _userDataStart; return isoPosition; } set { // Seek to the underlying ISO9660 position Seek(value, SeekOrigin.Begin); } } /// public override int Read(byte[] buffer, int offset, int count) { int totalRead = 0; int remaining = count; while (remaining > 0 && _position < _baseStream.Length) { // Determine location of current sector long baseStreamOffset = _position - (_position % Constants.CDROMSectorSize); // Set the current sector's mode and user data location SetState(baseStreamOffset); // Deal with case where base position is not in ISO stream long remainder = _position % Constants.CDROMSectorSize; long sectorOffset = remainder - _userDataStart; if (remainder < _userDataStart) { baseStreamOffset += _userDataStart; sectorOffset = 0; _position += _userDataStart; } else if (remainder >= _userDataEnd) { baseStreamOffset += Constants.CDROMSectorSize; sectorOffset = 0; _position += Constants.CDROMSectorSize - _userDataEnd + _userDataStart; } else { baseStreamOffset += remainder; } // Sanity check on read location before seeking if (baseStreamOffset < 0 || baseStreamOffset > _baseStream.Length) throw new ArgumentOutOfRangeException(nameof(offset), "Attempted to seek outside the stream boundaries."); // Seek to target position in base CDROM stream _baseStream.SeekIfPossible(baseStreamOffset, SeekOrigin.Begin); // Read the remaining bytes, up to max of one ISO sector (2048 bytes) int bytesToRead = (int)Math.Min(remaining, _isoSectorSize - sectorOffset); // Don't overshoot end of stream bytesToRead = (int)Math.Min(bytesToRead, _baseStream.Length - _position); // Finish reading if no more bytes to be read if (bytesToRead <= 0) break; // Read up to 2048 bytes from base CDROM stream int bytesRead = _baseStream.Read(buffer, offset + totalRead, bytesToRead); // Update state for base stream _position = _baseStream.Position; if (bytesToRead == (_isoSectorSize - sectorOffset)) _position += Constants.CDROMSectorSize - _userDataEnd + _userDataStart; // Update state for ISO stream totalRead += bytesRead; remaining -= bytesRead; if (bytesRead == 0) break; } return totalRead; } /// public override long Seek(long offset, SeekOrigin origin) { // Get the intended position for the ISO9660 stream long targetPosition = origin switch { SeekOrigin.Begin => offset, SeekOrigin.Current => Position + offset, SeekOrigin.End => Length + offset, _ => throw new ArgumentException("Invalid SeekOrigin.", nameof(origin)), }; // Get the number of ISO sectors before current position long newPosition = targetPosition / _isoSectorSize * Constants.CDROMSectorSize; // Set the current sector's mode and user data location SetState(newPosition); // Add the within-sector position newPosition += _userDataStart + (targetPosition % _isoSectorSize); if (newPosition < 0 || newPosition > _baseStream.Length) throw new ArgumentOutOfRangeException(nameof(offset), "Attempted to seek outside the stream boundaries."); _position = _baseStream.SeekIfPossible(newPosition, SeekOrigin.Begin); return Position; } /// /// Update the current stream state based on the location /// /// Sector location to update from private void SetState(long sectorLocation) { long current = _baseStream.Position; long modePosition = sectorLocation - (sectorLocation % Constants.CDROMSectorSize) + 15; // Get the current sector mode _baseStream.SeekIfPossible(modePosition, SeekOrigin.Begin); _currentMode = _baseStream.GetSectorMode(); // Set the user data location variables _userDataStart = _currentMode.GetUserDataStart(); _userDataEnd = _currentMode.GetUserDataEnd(); // _isoSectorSize = _currentMode.GetUserDataSize(); // Reset the stream position _baseStream.SeekIfPossible(current, SeekOrigin.Begin); } } } }