// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : Write.cs // Author(s) : Natalia Portillo // // Component : Disc image plugins. // // --[ Description ] ---------------------------------------------------------- // // Writes Alcohol 120% disc images. // // --[ License ] -------------------------------------------------------------- // // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as // published by the Free Software Foundation; either version 2.1 of the // License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, see . // // ---------------------------------------------------------------------------- // Copyright © 2011-2025 Natalia Portillo // ****************************************************************************/ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using Aaru.CommonTypes; using Aaru.CommonTypes.AaruMetadata; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Structs; using Aaru.Decoders.CD; using Aaru.Helpers; using Aaru.Logging; using TrackType = Aaru.CommonTypes.Enums.TrackType; namespace Aaru.Images; public sealed partial class Alcohol120 { #region IWritableOpticalImage Members /// public bool Create(string path, MediaType mediaType, Dictionary options, ulong sectors, uint sectorSize) { if(!SupportedMediaTypes.Contains(mediaType)) { ErrorMessage = string.Format(Localization.Unsupported_media_format_0, mediaType); return false; } _imageInfo = new ImageInfo { MediaType = mediaType, SectorSize = sectorSize, Sectors = sectors }; try { _descriptorStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None); _imageStream = new FileStream(Path.Combine(Path.GetDirectoryName(path) ?? "", Path.GetFileNameWithoutExtension(path)) + ".mdf", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); } catch(IOException ex) { ErrorMessage = string.Format(Localization.Could_not_create_new_image_file_exception_0, ex.Message); AaruLogging.Exception(ex, Localization.Could_not_create_new_image_file_exception_0, ex.Message); return false; } _imageInfo.MediaType = mediaType; _isDvd = mediaType switch { MediaType.CD or MediaType.CDDA or MediaType.CDEG or MediaType.CDG or MediaType.CDI or MediaType.CDMIDI or MediaType.CDMRW or MediaType.CDPLUS or MediaType.CDR or MediaType.CDROM or MediaType.CDROMXA or MediaType.CDRW or MediaType.CDV or MediaType.DTSCD or MediaType.JaguarCD or MediaType.MEGACD or MediaType.PS1CD or MediaType.PS2CD or MediaType.SuperCDROM2 or MediaType.SVCD or MediaType.SATURNCD or MediaType.ThreeDO or MediaType.VCD or MediaType.VCDHD or MediaType.NeoGeoCD or MediaType.PCFX or MediaType.CDTV or MediaType.CD32 or MediaType.Nuon or MediaType.Playdia or MediaType.Pippin or MediaType.FMTOWNS or MediaType.MilCD or MediaType.VideoNow or MediaType.VideoNowColor or MediaType.VideoNowXp or MediaType.CVD => false, _ => true }; _trackFlags = new Dictionary(); IsWriting = true; ErrorMessage = null; return true; } /// public bool WriteMediaTag(byte[] data, MediaTagType tag) { if(!IsWriting) { ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; return false; } switch(tag) { case MediaTagType.CD_FullTOC: if(_isDvd) { ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1, tag, _imageInfo.MediaType); return false; } _fullToc = data; return true; case MediaTagType.DVD_PFI: if(!_isDvd) { ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1, tag, _imageInfo.MediaType); return false; } _pfi = data; return true; case MediaTagType.DVD_DMI: if(!_isDvd) { ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1, tag, _imageInfo.MediaType); return false; } _dmi = data; return true; case MediaTagType.DVD_BCA: if(!_isDvd) { ErrorMessage = string.Format(Localization.Unsupported_media_tag_0_for_medium_type_1, tag, _imageInfo.MediaType); return false; } _bca = data; return true; default: ErrorMessage = string.Format(Localization.Unsupported_media_tag_0, tag); return false; } } /// public bool WriteSector(byte[] data, ulong sectorAddress) { if(!IsWriting) { ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; return false; } CommonTypes.Structs.Track track = _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector); if(!_isDvd) { ErrorMessage = Localization.Cannot_write_non_long_sectors_to_CD_images; return false; } if(track is null) { ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress); return false; } if(track.BytesPerSector != track.RawBytesPerSector) { ErrorMessage = Localization.Invalid_write_mode_for_this_sector; return false; } if(data.Length != track.RawBytesPerSector) { ErrorMessage = Localization.Incorrect_data_size; return false; } _imageStream.Seek((long)(track.FileOffset + (sectorAddress - track.StartSector) * (ulong)track.RawBytesPerSector), SeekOrigin.Begin); _imageStream.Write(data, 0, data.Length); return true; } /// public bool WriteSectors(byte[] data, ulong sectorAddress, uint length) { if(!IsWriting) { ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; return false; } if(!_isDvd) { ErrorMessage = Localization.Cannot_write_non_long_sectors_to_CD_images; return false; } CommonTypes.Structs.Track track = _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector); if(track is null) { ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress); return false; } if(track.BytesPerSector != track.RawBytesPerSector) { ErrorMessage = Localization.Invalid_write_mode_for_this_sector; return false; } if(sectorAddress + length > track.EndSector + 1) { ErrorMessage = Localization.Cant_cross_tracks; return false; } if(data.Length % track.RawBytesPerSector != 0) { ErrorMessage = Localization.Incorrect_data_size; return false; } switch(track.SubchannelType) { case TrackSubchannelType.None: _imageStream.Seek((long)(track.FileOffset + (sectorAddress - track.StartSector) * (ulong)track.RawBytesPerSector), SeekOrigin.Begin); _imageStream.Write(data, 0, data.Length); ErrorMessage = ""; return true; case TrackSubchannelType.Raw: case TrackSubchannelType.RawInterleaved: _imageStream.Seek((long)(track.FileOffset + (sectorAddress - track.StartSector) * (ulong)(track.RawBytesPerSector + 96)), SeekOrigin.Begin); for(uint i = 0; i < length; i++) { _imageStream.Write(data, (int)(i * track.RawBytesPerSector), track.RawBytesPerSector); _imageStream.Position += 96; } ErrorMessage = ""; return true; default: ErrorMessage = Localization.Invalid_subchannel_mode_for_this_sector; return false; } } /// public bool WriteSectorLong(byte[] data, ulong sectorAddress) { if(!IsWriting) { ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; return false; } CommonTypes.Structs.Track track = _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector); if(track is null) { ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress); return false; } if(data.Length != track.RawBytesPerSector) { ErrorMessage = Localization.Incorrect_data_size; return false; } uint subchannelSize = (uint)(track.SubchannelType != TrackSubchannelType.None ? 96 : 0); _imageStream.Seek((long)(track.FileOffset + (sectorAddress - track.StartSector) * (ulong)(track.RawBytesPerSector + subchannelSize)), SeekOrigin.Begin); _imageStream.Write(data, 0, data.Length); return true; } /// public bool WriteSectorsLong(byte[] data, ulong sectorAddress, uint length) { if(!IsWriting) { ErrorMessage = Localization.Tried_to_write_on_a_non_writable_image; return false; } CommonTypes.Structs.Track track = _writingTracks.FirstOrDefault(trk => sectorAddress >= trk.StartSector && sectorAddress <= trk.EndSector); if(track is null) { ErrorMessage = string.Format(Localization.Cant_find_track_containing_0, sectorAddress); return false; } if(sectorAddress + length > track.EndSector + 1) { ErrorMessage = Localization.Cant_cross_tracks; return false; } if(data.Length % track.RawBytesPerSector != 0) { ErrorMessage = Localization.Incorrect_data_size; return false; } uint subchannelSize = (uint)(track.SubchannelType != TrackSubchannelType.None ? 96 : 0); for(uint i = 0; i < length; i++) { _imageStream.Seek((long)(track.FileOffset + (i + sectorAddress - track.StartSector) * (ulong)(track.RawBytesPerSector + subchannelSize)), SeekOrigin.Begin); _imageStream.Write(data, (int)(i * track.RawBytesPerSector), track.RawBytesPerSector); } return true; } /// public bool SetTracks(List tracks) { ulong currentDataOffset = 0; _writingTracks = []; if(!_isDvd) { CommonTypes.Structs.Track[] tmpTracks = tracks.OrderBy(t => t.Sequence).ToArray(); for(int i = 1; i < tmpTracks.Length; i++) { CommonTypes.Structs.Track firstTrackInSession = tracks.FirstOrDefault(t => t.Session == tmpTracks[i].Session); if(firstTrackInSession is null) continue; if(tmpTracks[i].Sequence == firstTrackInSession.Sequence) { if(tmpTracks[i].Sequence > 1) tmpTracks[i].StartSector += 150; continue; } tmpTracks[i - 1].EndSector += tmpTracks[i].Pregap; tmpTracks[i].Pregap = 0; tmpTracks[i].StartSector = tmpTracks[i - 1].EndSector + 1; } tracks = tmpTracks.ToList(); } foreach(CommonTypes.Structs.Track track in tracks.OrderBy(t => t.Sequence)) { uint subchannelSize; switch(track.SubchannelType) { case TrackSubchannelType.None: subchannelSize = 0; break; case TrackSubchannelType.Raw: case TrackSubchannelType.RawInterleaved: subchannelSize = 96; break; default: ErrorMessage = string.Format(Localization.Unsupported_subchannel_type_0, track.SubchannelType); return false; } track.FileOffset = currentDataOffset; currentDataOffset += (ulong)(track.RawBytesPerSector + subchannelSize) * (track.EndSector - track.StartSector + 1); _writingTracks.Add(track); } return true; } /// public bool Close() { if(!IsWriting) { ErrorMessage = Localization.Image_is_not_opened_for_writing; return false; } byte sessions = byte.MinValue; foreach(CommonTypes.Structs.Track t in _writingTracks.Where(t => t.Session > byte.MinValue)) sessions = (byte)t.Session; var header = new Header { signature = _alcoholSignature, version = [1, 5], type = MediaTypeToMediumType(_imageInfo.MediaType), sessions = sessions, structuresOffset = (uint)(_pfi == null ? 0 : 96), sessionOffset = (uint)(_pfi == null ? 96 : 4196), unknown1 = new ushort[2], unknown2 = new uint[2], unknown3 = new uint[6], unknown4 = new uint[3] }; // Alcohol sets this always, Daemon Tool expects this header.unknown1[0] = 2; _alcSessions = new Dictionary(); _alcTracks = new Dictionary(); _alcToc = new Dictionary>(); _writingTracks = _writingTracks.OrderBy(t => t.Session).ThenBy(t => t.Sequence).ToList(); _alcTrackExtras = new Dictionary(); long currentTrackOffset = header.sessionOffset + Marshal.SizeOf() * sessions; byte[] tmpToc = null; if(_fullToc != null) { byte[] fullTocSize = BigEndianBitConverter.GetBytes((short)_fullToc.Length); tmpToc = new byte[_fullToc.Length + 2]; Array.Copy(_fullToc, 0, tmpToc, 2, _fullToc.Length); tmpToc[0] = fullTocSize[0]; tmpToc[1] = fullTocSize[1]; } FullTOC.CDFullTOC? decodedToc = FullTOC.Decode(tmpToc); long currentExtraOffset = currentTrackOffset; int extraCount = 0; for(int i = 1; i <= sessions; i++) { if(decodedToc.HasValue) { extraCount += decodedToc.Value.TrackDescriptors.Count(t => t.SessionNumber == i); currentExtraOffset += Marshal.SizeOf() * decodedToc.Value.TrackDescriptors.Count(t => t.SessionNumber == i); } else { currentExtraOffset += Marshal.SizeOf() * 3; extraCount += 3; currentExtraOffset += Marshal.SizeOf() * _writingTracks.Count(t => t.Session == i); extraCount += _writingTracks.Count(t => t.Session == i); if(i >= sessions) continue; currentExtraOffset += Marshal.SizeOf() * 2; extraCount += 2; } } long footerOffset = currentExtraOffset + Marshal.SizeOf() * extraCount; if(_bca != null) { header.bcaOffset = (uint)footerOffset; footerOffset += _bca.Length; } if(_isDvd) { _alcSessions.Add(1, new Session { sessionEnd = (int)(_writingTracks[0].EndSector - _writingTracks[0].StartSector + 1), sessionSequence = 1, allBlocks = 1, nonTrackBlocks = 3, firstTrack = 1, lastTrack = 1, trackOffset = 4220 }); footerOffset = 4300; if(_bca != null) footerOffset += _bca.Length; _alcTracks.Add(1, new Track { mode = TrackMode.DVD, adrCtl = 20, point = 1, extraOffset = (uint)(_writingTracks[0].EndSector - _writingTracks[0].StartSector + 1), sectorSize = 2048, files = 1, footerOffset = (uint)footerOffset, unknown = new byte[18], unknown2 = new byte[24] }); _alcToc.Add(1, _alcTracks); } else { for(int i = 1; i <= sessions; i++) { CommonTypes.Structs.Track firstTrack = _writingTracks.First(t => t.Session == i); CommonTypes.Structs.Track lastTrack = _writingTracks.Last(t => t.Session == i); _alcSessions.Add(i, new Session { sessionStart = (int)firstTrack.StartSector - 150, sessionEnd = (int)lastTrack.EndSector + 1, sessionSequence = (ushort)i, allBlocks = (byte)(decodedToc?.TrackDescriptors.Count(t => t.SessionNumber == i) ?? _writingTracks.Count(t => t.Session == i) + 3), nonTrackBlocks = (byte)(decodedToc?.TrackDescriptors.Count(t => t.SessionNumber == i && t.POINT is >= 0xA0 and <= 0xAF) ?? 3), firstTrack = (ushort)firstTrack.Sequence, lastTrack = (ushort)lastTrack.Sequence, trackOffset = (uint)currentTrackOffset }); Dictionary thisSessionTracks = new(); _trackFlags.TryGetValue((byte)firstTrack.Sequence, out byte firstTrackControl); _trackFlags.TryGetValue((byte)lastTrack.Sequence, out byte lastTrackControl); if(firstTrackControl == 0 && firstTrack.Type != TrackType.Audio) firstTrackControl = (byte)CdFlags.DataTrack; if(lastTrackControl == 0 && lastTrack.Type != TrackType.Audio) lastTrackControl = (byte)CdFlags.DataTrack; (byte minute, byte second, byte frame) leadinPmsf = LbaToMsf(lastTrack.EndSector + 1); if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT is >= 0xA0 and <= 0xAF) == true) { foreach(FullTOC.TrackDataDescriptor tocTrk in decodedToc.Value.TrackDescriptors.Where(t => t.SessionNumber == i && t.POINT is >= 0xA0 and <= 0xAF)) { thisSessionTracks.Add(tocTrk.POINT, new Track { adrCtl = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL), tno = tocTrk.TNO, point = tocTrk.POINT, min = tocTrk.Min, sec = tocTrk.Sec, frame = tocTrk.Frame, zero = tocTrk.Zero, pmin = tocTrk.PMIN, psec = tocTrk.PSEC, pframe = tocTrk.PFRAME, mode = TrackMode.NoData, unknown = new byte[18], unknown2 = new byte[24], extraOffset = (uint)currentExtraOffset }); currentTrackOffset += Marshal.SizeOf(); currentExtraOffset += Marshal.SizeOf(); } } else { thisSessionTracks.Add(0xA0, new Track { adrCtl = (byte)((1 << 4) + firstTrackControl), pmin = (byte)firstTrack.Sequence, mode = TrackMode.NoData, point = 0xA0, unknown = new byte[18], unknown2 = new byte[24], psec = (byte)(_imageInfo.MediaType == MediaType.CDI ? 0x10 : _writingTracks.Any(t => t.Type is TrackType .CdMode2Form1 or TrackType.CdMode2Form2 or TrackType.CdMode2Formless) ? 0x20 : 0), extraOffset = (uint)currentExtraOffset }); thisSessionTracks.Add(0xA1, new Track { adrCtl = (byte)((1 << 4) + lastTrackControl), pmin = (byte)lastTrack.Sequence, mode = TrackMode.NoData, point = 0xA1, unknown = new byte[18], unknown2 = new byte[24], extraOffset = (uint)currentExtraOffset }); thisSessionTracks.Add(0xA2, new Track { adrCtl = (byte)((1 << 4) + firstTrackControl), zero = 0, pmin = leadinPmsf.minute, psec = leadinPmsf.second, pframe = leadinPmsf.frame, mode = TrackMode.NoData, point = 0xA2, unknown = new byte[18], unknown2 = new byte[24], extraOffset = (uint)currentExtraOffset }); currentExtraOffset += Marshal.SizeOf() * 3; currentTrackOffset += Marshal.SizeOf() * 3; } foreach(CommonTypes.Structs.Track track in _writingTracks.Where(t => t.Session == i) .OrderBy(t => t.Sequence)) { var alcTrk = new Track(); if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT == track.Sequence) == true) { FullTOC.TrackDataDescriptor tocTrk = decodedToc.Value.TrackDescriptors.First(t => t.SessionNumber == i && t.POINT == track.Sequence); alcTrk.adrCtl = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL); alcTrk.tno = tocTrk.TNO; alcTrk.point = tocTrk.POINT; alcTrk.min = tocTrk.Min; alcTrk.sec = tocTrk.Sec; alcTrk.frame = tocTrk.Frame; alcTrk.zero = tocTrk.Zero; alcTrk.pmin = tocTrk.PMIN; alcTrk.psec = tocTrk.PSEC; alcTrk.pframe = tocTrk.PFRAME; } else { (byte minute, byte second, byte frame) msf = LbaToMsf((ulong)track.Indexes[1]); _trackFlags.TryGetValue((byte)track.Sequence, out byte trackControl); if(trackControl == 0 && track.Type != TrackType.Audio) trackControl = (byte)CdFlags.DataTrack; alcTrk.adrCtl = (byte)((1 << 4) + trackControl); alcTrk.point = (byte)track.Sequence; alcTrk.zero = 0; alcTrk.pmin = msf.minute; alcTrk.psec = msf.second; alcTrk.pframe = msf.frame; } alcTrk.mode = TrackTypeToTrackMode(track.Type); alcTrk.subMode = track.SubchannelType != TrackSubchannelType.None ? SubchannelMode.Interleaved : SubchannelMode.None; alcTrk.sectorSize = (ushort)(track.RawBytesPerSector + (track.SubchannelType != TrackSubchannelType.None ? 96 : 0)); alcTrk.startLba = (uint)(track.StartSector + track.Pregap); alcTrk.startOffset = track.FileOffset + alcTrk.sectorSize * track.Pregap; alcTrk.files = 1; alcTrk.extraOffset = (uint)currentExtraOffset; alcTrk.footerOffset = (uint)footerOffset; if(track.Sequence == firstTrack.Sequence) { alcTrk.startLba -= (uint)track.Pregap; alcTrk.startOffset -= alcTrk.sectorSize * track.Pregap; } // Alcohol seems to set that for all CD tracks // Daemon Tools expect it to be like this alcTrk.unknown = [ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]; alcTrk.unknown2 = new byte[24]; thisSessionTracks.Add((int)track.Sequence, alcTrk); currentTrackOffset += Marshal.SizeOf(); currentExtraOffset += Marshal.SizeOf(); var trkExtra = new TrackExtra { sectors = (uint)(track.EndSector - track.StartSector + 1) }; if(track.Sequence == firstTrack.Sequence) trkExtra.pregap = 150; // When track mode changes there's a mandatory gap, Alcohol needs it else if(thisSessionTracks.TryGetValue((int)(track.Sequence - 1), out Track previousTrack) && _alcTrackExtras.TryGetValue((int)(track.Sequence - 1), out TrackExtra previousExtra) && previousTrack.mode != alcTrk.mode) { previousExtra.sectors -= 150; trkExtra.pregap = 150; _alcTrackExtras.Remove((int)(track.Sequence - 1)); _alcTrackExtras.Add((int)(track.Sequence - 1), previousExtra); } else trkExtra.pregap = 0; _alcTrackExtras.Add((int)track.Sequence, trkExtra); } if(decodedToc?.TrackDescriptors.Any(t => t.SessionNumber == i && t.POINT >= 0xB0) == true) { foreach(FullTOC.TrackDataDescriptor tocTrk in decodedToc.Value.TrackDescriptors.Where(t => t.SessionNumber == i && t.POINT >= 0xB0)) { thisSessionTracks.Add(tocTrk.POINT, new Track { adrCtl = (byte)((tocTrk.ADR << 4) + tocTrk.CONTROL), tno = tocTrk.TNO, point = tocTrk.POINT, min = tocTrk.Min, sec = tocTrk.Sec, frame = tocTrk.Frame, zero = tocTrk.Zero, pmin = tocTrk.PMIN, psec = tocTrk.PSEC, pframe = tocTrk.PFRAME, mode = TrackMode.NoData, unknown = new byte[18], unknown2 = new byte[24], extraOffset = (uint)currentExtraOffset }); currentExtraOffset += Marshal.SizeOf(); currentTrackOffset += Marshal.SizeOf(); } } else if(i < sessions) { (byte minute, byte second, byte frame) leadoutAmsf = LbaToMsf(_writingTracks.First(t => t.Session == i + 1).StartSector - 150); (byte minute, byte second, byte frame) leadoutPmsf = LbaToMsf(_writingTracks.OrderBy(t => t.Session).ThenBy(t => t.Sequence).Last().StartSector); thisSessionTracks.Add(0xB0, new Track { point = 0xB0, adrCtl = 0x50, zero = 0, min = leadoutAmsf.minute, sec = leadoutAmsf.second, frame = leadoutAmsf.frame, pmin = leadoutPmsf.minute, psec = leadoutPmsf.second, pframe = leadoutPmsf.frame, unknown = new byte[18], unknown2 = new byte[24] }); thisSessionTracks.Add(0xC0, new Track { point = 0xC0, adrCtl = 0x50, min = 128, pmin = 97, psec = 25, unknown = new byte[18], unknown2 = new byte[24] }); currentTrackOffset += Marshal.SizeOf() * 2; } _alcToc.Add(i, thisSessionTracks); } } _alcFooter = new Footer { filenameOffset = (uint)(footerOffset + Marshal.SizeOf