// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // // Filename : ProDOS.cs // Author(s) : Natalia Portillo // // Component : Apple ProDOS filesystem plugin. // // --[ Description ] ---------------------------------------------------------- // // Identifies the Apple ProDOS filesystem and shows information. // // --[ 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-2022 Natalia Portillo // ****************************************************************************/ // ReSharper disable NotAccessedField.Local namespace Aaru.Filesystems; using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.Console; using Claunia.Encoding; using Schemas; using Encoding = System.Text.Encoding; // Information from Apple ProDOS 8 Technical Reference /// /// Implements detection of Apple ProDOS filesystem [SuppressMessage("ReSharper", "UnusedMember.Local"), SuppressMessage("ReSharper", "UnusedType.Local")] public sealed class ProDOSPlugin : IFilesystem { const byte EMPTY_STORAGE_TYPE = 0x00; /// A file that occupies one block or less const byte SEEDLING_FILE_TYPE = 0x01; /// A file that occupies between 2 and 256 blocks const byte SAPLING_FILE_TYPE = 0x02; /// A file that occupies between 257 and 32768 blocks const byte TREE_FILE_TYPE = 0x03; const byte PASCAL_AREA_TYPE = 0x04; const byte SUBDIRECTORY_TYPE = 0x0D; const byte SUBDIRECTORY_HEADER_TYPE = 0x0E; const byte ROOT_DIRECTORY_TYPE = 0x0F; const byte VERSION1 = 0x00; const uint YEAR_MASK = 0xFE000000; const uint MONTH_MASK = 0x1E00000; const uint DAY_MASK = 0x1F0000; const uint HOUR_MASK = 0x1F00; const uint MINUTE_MASK = 0x3F; const byte DESTROY_ATTRIBUTE = 0x80; const byte RENAME_ATTRIBUTE = 0x40; const byte BACKUP_ATTRIBUTE = 0x20; const byte WRITE_ATTRIBUTE = 0x02; const byte READ_ATTRIBUTE = 0x01; const byte RESERVED_ATTRIBUTE_MASK = 0x1C; const byte STORAGE_TYPE_MASK = 0xF0; const byte NAME_LENGTH_MASK = 0x0F; const byte ENTRY_LENGTH = 0x27; const byte ENTRIES_PER_BLOCK = 0x0D; /// public FileSystemType XmlFsType { get; private set; } /// public Encoding Encoding { get; private set; } /// public string Name => "Apple ProDOS filesystem"; /// public Guid Id => new("43874265-7B8A-4739-BCF7-07F80D5932BF"); /// public string Author => "Natalia Portillo"; /// public bool Identify(IMediaImage imagePlugin, Partition partition) { if(partition.Length < 3) return false; var multiplier = (uint)(imagePlugin.Info.SectorSize == 256 ? 2 : 1); // Blocks 0 and 1 are boot code ErrorNumber errno = imagePlugin.ReadSectors(2 * multiplier + partition.Start, multiplier, out byte[] rootDirectoryKeyBlock); if(errno != ErrorNumber.NoError) return false; var apmFromHddOnCd = false; if(imagePlugin.Info.SectorSize is 2352 or 2448 or 2048) { errno = imagePlugin.ReadSectors(partition.Start, 2, out byte[] tmp); if(errno != ErrorNumber.NoError) return false; foreach(int offset in new[] { 0, 0x200, 0x400, 0x600, 0x800, 0xA00 }.Where(offset => tmp.Length > offset + 0x200 && BitConverter.ToUInt16(tmp, offset) == 0 && (byte)((tmp[offset + 0x04] & STORAGE_TYPE_MASK) >> 4) == ROOT_DIRECTORY_TYPE && tmp[offset + 0x23] == ENTRY_LENGTH && tmp[offset + 0x24] == ENTRIES_PER_BLOCK)) { Array.Copy(tmp, offset, rootDirectoryKeyBlock, 0, 0x200); apmFromHddOnCd = true; break; } } var prePointer = BitConverter.ToUInt16(rootDirectoryKeyBlock, 0); AaruConsole.DebugWriteLine("ProDOS plugin", "prePointer = {0}", prePointer); if(prePointer != 0) return false; var storageType = (byte)((rootDirectoryKeyBlock[0x04] & STORAGE_TYPE_MASK) >> 4); AaruConsole.DebugWriteLine("ProDOS plugin", "storage_type = {0}", storageType); if(storageType != ROOT_DIRECTORY_TYPE) return false; byte entryLength = rootDirectoryKeyBlock[0x23]; AaruConsole.DebugWriteLine("ProDOS plugin", "entry_length = {0}", entryLength); if(entryLength != ENTRY_LENGTH) return false; byte entriesPerBlock = rootDirectoryKeyBlock[0x24]; AaruConsole.DebugWriteLine("ProDOS plugin", "entries_per_block = {0}", entriesPerBlock); if(entriesPerBlock != ENTRIES_PER_BLOCK) return false; var bitMapPointer = BitConverter.ToUInt16(rootDirectoryKeyBlock, 0x27); AaruConsole.DebugWriteLine("ProDOS plugin", "bit_map_pointer = {0}", bitMapPointer); if(bitMapPointer > partition.End) return false; var totalBlocks = BitConverter.ToUInt16(rootDirectoryKeyBlock, 0x29); if(apmFromHddOnCd) totalBlocks /= 4; AaruConsole.DebugWriteLine("ProDOS plugin", "{0} <= ({1} - {2} + 1)? {3}", totalBlocks, partition.End, partition.Start, totalBlocks <= partition.End - partition.Start + 1); return totalBlocks <= partition.End - partition.Start + 1; } /// public void GetInformation(IMediaImage imagePlugin, Partition partition, out string information, Encoding encoding) { Encoding = encoding ?? new Apple2c(); information = ""; var sbInformation = new StringBuilder(); var multiplier = (uint)(imagePlugin.Info.SectorSize == 256 ? 2 : 1); // Blocks 0 and 1 are boot code ErrorNumber errno = imagePlugin.ReadSectors(2 * multiplier + partition.Start, multiplier, out byte[] rootDirectoryKeyBlockBytes); if(errno != ErrorNumber.NoError) return; var apmFromHddOnCd = false; if(imagePlugin.Info.SectorSize is 2352 or 2448 or 2048) { errno = imagePlugin.ReadSectors(partition.Start, 2, out byte[] tmp); if(errno != ErrorNumber.NoError) return; foreach(int offset in new[] { 0, 0x200, 0x400, 0x600, 0x800, 0xA00 }.Where(offset => BitConverter.ToUInt16(tmp, offset) == 0 && (byte)((tmp[offset + 0x04] & STORAGE_TYPE_MASK) >> 4) == ROOT_DIRECTORY_TYPE && tmp[offset + 0x23] == ENTRY_LENGTH && tmp[offset + 0x24] == ENTRIES_PER_BLOCK)) { Array.Copy(tmp, offset, rootDirectoryKeyBlockBytes, 0, 0x200); apmFromHddOnCd = true; break; } } var rootDirectoryKeyBlock = new RootDirectoryKeyBlock { header = new RootDirectoryHeader(), zero = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x00), next_pointer = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x02) }; rootDirectoryKeyBlock.header.storage_type = (byte)((rootDirectoryKeyBlockBytes[0x04] & STORAGE_TYPE_MASK) >> 4); rootDirectoryKeyBlock.header.name_length = (byte)(rootDirectoryKeyBlockBytes[0x04] & NAME_LENGTH_MASK); var temporal = new byte[rootDirectoryKeyBlock.header.name_length]; Array.Copy(rootDirectoryKeyBlockBytes, 0x05, temporal, 0, rootDirectoryKeyBlock.header.name_length); rootDirectoryKeyBlock.header.volume_name = Encoding.GetString(temporal); rootDirectoryKeyBlock.header.reserved = BitConverter.ToUInt64(rootDirectoryKeyBlockBytes, 0x14); var tempTimestampLeft = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x1C); var tempTimestampRight = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x1E); bool dateCorrect; try { var tempTimestamp = (uint)((tempTimestampLeft << 16) + tempTimestampRight); var year = (int)((tempTimestamp & YEAR_MASK) >> 25); var month = (int)((tempTimestamp & MONTH_MASK) >> 21); var day = (int)((tempTimestamp & DAY_MASK) >> 16); var hour = (int)((tempTimestamp & HOUR_MASK) >> 8); var minute = (int)(tempTimestamp & MINUTE_MASK); year += 1900; if(year < 1940) year += 100; AaruConsole.DebugWriteLine("ProDOS plugin", "temp_timestamp_left = 0x{0:X4}", tempTimestampLeft); AaruConsole.DebugWriteLine("ProDOS plugin", "temp_timestamp_right = 0x{0:X4}", tempTimestampRight); AaruConsole.DebugWriteLine("ProDOS plugin", "temp_timestamp = 0x{0:X8}", tempTimestamp); AaruConsole.DebugWriteLine("ProDOS plugin", "Datetime field year {0}, month {1}, day {2}, hour {3}, minute {4}.", year, month, day, hour, minute); rootDirectoryKeyBlock.header.creation_time = new DateTime(year, month, day, hour, minute, 0); dateCorrect = true; } catch(ArgumentOutOfRangeException) { dateCorrect = false; } rootDirectoryKeyBlock.header.version = rootDirectoryKeyBlockBytes[0x20]; rootDirectoryKeyBlock.header.min_version = rootDirectoryKeyBlockBytes[0x21]; rootDirectoryKeyBlock.header.access = rootDirectoryKeyBlockBytes[0x22]; rootDirectoryKeyBlock.header.entry_length = rootDirectoryKeyBlockBytes[0x23]; rootDirectoryKeyBlock.header.entries_per_block = rootDirectoryKeyBlockBytes[0x24]; rootDirectoryKeyBlock.header.file_count = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x25); rootDirectoryKeyBlock.header.bit_map_pointer = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x27); rootDirectoryKeyBlock.header.total_blocks = BitConverter.ToUInt16(rootDirectoryKeyBlockBytes, 0x29); if(apmFromHddOnCd) sbInformation.AppendLine("ProDOS uses 512 bytes/sector while devices uses 2048 bytes/sector.").AppendLine(); if(rootDirectoryKeyBlock.header.version != VERSION1 || rootDirectoryKeyBlock.header.min_version != VERSION1) { sbInformation.AppendLine("Warning! Detected unknown ProDOS version ProDOS filesystem."); sbInformation.AppendLine("All of the following information may be incorrect"); } if(rootDirectoryKeyBlock.header.version == VERSION1) sbInformation.AppendLine("ProDOS version 1 used to create this volume."); else sbInformation.AppendFormat("Unknown ProDOS version with field {0} used to create this volume.", rootDirectoryKeyBlock.header.version).AppendLine(); if(rootDirectoryKeyBlock.header.min_version == VERSION1) sbInformation.AppendLine("ProDOS version 1 at least required for reading this volume."); else sbInformation. AppendFormat("Unknown ProDOS version with field {0} is at least required for reading this volume.", rootDirectoryKeyBlock.header.min_version).AppendLine(); sbInformation.AppendFormat("Volume name is {0}", rootDirectoryKeyBlock.header.volume_name).AppendLine(); if(dateCorrect) sbInformation.AppendFormat("Volume created on {0}", rootDirectoryKeyBlock.header.creation_time). AppendLine(); sbInformation.AppendFormat("{0} bytes per directory entry", rootDirectoryKeyBlock.header.entry_length). AppendLine(); sbInformation.AppendFormat("{0} entries per directory block", rootDirectoryKeyBlock.header.entries_per_block). AppendLine(); sbInformation.AppendFormat("{0} files in root directory", rootDirectoryKeyBlock.header.file_count).AppendLine(); sbInformation.AppendFormat("{0} blocks in volume", rootDirectoryKeyBlock.header.total_blocks).AppendLine(); sbInformation.AppendFormat("Bitmap starts at block {0}", rootDirectoryKeyBlock.header.bit_map_pointer). AppendLine(); if((rootDirectoryKeyBlock.header.access & READ_ATTRIBUTE) == READ_ATTRIBUTE) sbInformation.AppendLine("Volume can be read"); if((rootDirectoryKeyBlock.header.access & WRITE_ATTRIBUTE) == WRITE_ATTRIBUTE) sbInformation.AppendLine("Volume can be written"); if((rootDirectoryKeyBlock.header.access & RENAME_ATTRIBUTE) == RENAME_ATTRIBUTE) sbInformation.AppendLine("Volume can be renamed"); if((rootDirectoryKeyBlock.header.access & DESTROY_ATTRIBUTE) == DESTROY_ATTRIBUTE) sbInformation.AppendLine("Volume can be destroyed"); if((rootDirectoryKeyBlock.header.access & BACKUP_ATTRIBUTE) == BACKUP_ATTRIBUTE) sbInformation.AppendLine("Volume must be backed up"); if((rootDirectoryKeyBlock.header.access & RESERVED_ATTRIBUTE_MASK) != 0) AaruConsole.DebugWriteLine("ProDOS plugin", "Reserved attributes are set: {0:X2}", rootDirectoryKeyBlock.header.access); information = sbInformation.ToString(); XmlFsType = new FileSystemType { VolumeName = rootDirectoryKeyBlock.header.volume_name, Files = rootDirectoryKeyBlock.header.file_count, FilesSpecified = true, Clusters = rootDirectoryKeyBlock.header.total_blocks, Type = "ProDOS" }; XmlFsType.ClusterSize = (uint)((partition.End - partition.Start + 1) * imagePlugin.Info.SectorSize / XmlFsType.Clusters); if(!dateCorrect) return; XmlFsType.CreationDate = rootDirectoryKeyBlock.header.creation_time; XmlFsType.CreationDateSpecified = true; } /// ProDOS directory entry, decoded structure [SuppressMessage("ReSharper", "InconsistentNaming")] struct Entry { /// Type of file pointed by this entry Offset 0x00, mask 0xF0 public byte storage_type; /// Length of name_length pascal string Offset 0x00, mask 0x0F public byte name_length; /// Pascal string of file name Offset 0x01, 15 bytes public string file_name; /// Descriptor of internal structure of the file Offset 0x10, 1 byte public byte file_type; /// /// Block address of master index block for tree files. Block address of index block for sapling files. Block /// address of block for seedling files. Offset 0x11, 2 bytes /// public ushort key_pointer; /// Blocks used by file or directory, including index blocks. Offset 0x13, 2 bytes public ushort blocks_used; /// Size of file in bytes Offset 0x15, 3 bytes public uint EOF; /// File creation datetime Offset 0x18, 4 bytes public DateTime creation_time; /// Version of ProDOS that created this file Offset 0x1C, 1 byte public byte version; /// Minimum version of ProDOS needed to access this file Offset 0x1D, 1 byte public byte min_version; /// File permissions Offset 0x1E, 1 byte public byte access; /// General purpose field to store additional information about file format Offset 0x1F, 2 bytes public ushort aux_type; /// File last modification date time Offset 0x21, 4 bytes public DateTime last_mod; /// Block address pointer to key block of the directory containing this entry Offset 0x25, 2 bytes public ushort header_pointer; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct RootDirectoryHeader { /// Constant 0x0F Offset 0x04, mask 0xF0 public byte storage_type; /// Length of volume_name pascal string Offset 0x04, mask 0x0F public byte name_length; /// The name of the volume. Offset 0x05, 15 bytes public string volume_name; /// Reserved for future expansion Offset 0x14, 8 bytes public ulong reserved; /// Creation time of the volume Offset 0x1C, 4 bytes public DateTime creation_time; /// Version number of the volume format Offset 0x20, 1 byte public byte version; /// Reserved for future use Offset 0x21, 1 byte public byte min_version; /// Permissions for the volume Offset 0x22, 1 byte public byte access; /// Length of an entry in this directory Const 0x27 Offset 0x23, 1 byte public byte entry_length; /// Number of entries per block Const 0x0D Offset 0x24, 1 byte public byte entries_per_block; /// Number of active files in this directory Offset 0x25, 2 bytes public ushort file_count; /// /// Block address of the first block of the volume's bitmap, one for every 4096 blocks or fraction Offset 0x27, 2 /// bytes /// public ushort bit_map_pointer; /// Total number of blocks in the volume Offset 0x29, 2 bytes public ushort total_blocks; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct DirectoryHeader { /// Constant 0x0E Offset 0x04, mask 0xF0 public byte storage_type; /// Length of volume_name pascal string Offset 0x04, mask 0x0F public byte name_length; /// The name of the directory. Offset 0x05, 15 bytes public string directory_name; /// Reserved for future expansion Offset 0x14, 8 bytes public ulong reserved; /// Creation time of the volume Offset 0x1C, 4 bytes public DateTime creation_time; /// Version number of the volume format Offset 0x20, 1 byte public byte version; /// Reserved for future use Offset 0x21, 1 byte public byte min_version; /// Permissions for the volume Offset 0x22, 1 byte public byte access; /// Length of an entry in this directory Const 0x27 Offset 0x23, 1 byte public byte entry_length; /// Number of entries per block Const 0x0D Offset 0x24, 1 byte public byte entries_per_block; /// Number of active files in this directory Offset 0x25, 2 bytes public ushort file_count; /// Block address of parent directory block that contains this entry Offset 0x27, 2 bytes public ushort parent_pointer; /// Entry number within the block indicated in parent_pointer Offset 0x29, 1 byte public byte parent_entry_number; /// Length of the entry that holds this directory, in the parent entry Const 0x27 Offset 0x2A, 1 byte public byte parent_entry_length; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct DirectoryKeyBlock { /// Always 0 Offset 0x00, 2 bytes public ushort zero; /// Pointer to next directory block, 0 if last Offset 0x02, 2 bytes public ushort next_pointer; /// Directory header Offset 0x04, 39 bytes public DirectoryHeader header; /// Directory entries Offset 0x2F, 39 bytes each, 12 entries public Entry[] entries; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct RootDirectoryKeyBlock { /// Always 0 Offset 0x00, 2 bytes public ushort zero; /// Pointer to next directory block, 0 if last Offset 0x02, 2 bytes public ushort next_pointer; /// Directory header Offset 0x04, 39 bytes public RootDirectoryHeader header; /// Directory entries Offset 0x2F, 39 bytes each, 12 entries public Entry[] entries; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct DirectoryBlock { /// Pointer to previous directory block Offset 0x00, 2 bytes public ushort zero; /// Pointer to next directory block, 0 if last Offset 0x02, 2 bytes public ushort next_pointer; /// Directory entries Offset 0x2F, 39 bytes each, 13 entries public Entry[] entries; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct IndexBlock { /// Up to 256 pointers to blocks, 0 to indicate the block is sparsed (non-allocated) public ushort[] block_pointer; } [SuppressMessage("ReSharper", "InconsistentNaming")] struct MasterIndexBlock { /// Up to 128 pointers to index blocks public ushort[] index_block_pointer; } }