diff --git a/Aaru.Filesystems/CBM/CBM.cs b/Aaru.Filesystems/CBM/CBM.cs index f57e1fe61..7224ee55b 100644 --- a/Aaru.Filesystems/CBM/CBM.cs +++ b/Aaru.Filesystems/CBM/CBM.cs @@ -27,15 +27,27 @@ // ****************************************************************************/ using System; +using System.Collections.Generic; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; namespace Aaru.Filesystems; /// /// Implements detection of the filesystem used in 8-bit Commodore microcomputers -public sealed partial class CBM : IFilesystem +public sealed partial class CBM : IReadOnlyFilesystem { -#region IFilesystem Members + byte[] _bam; + Dictionary _cache; + bool _debug; + byte[] _diskHeader; + bool _mounted; + byte[] _root; + FileSystemInfo _statfs; + +#region IReadOnlyFilesystem Members /// public string Name => Localization.CBM_Name; @@ -46,5 +58,41 @@ public sealed partial class CBM : IFilesystem /// public string Author => Authors.NataliaPortillo; + /// + public ErrorNumber ListXAttr(string path, out List xattrs) + { + xattrs = null; + + return ErrorNumber.NotSupported; + } + + /// + public ErrorNumber GetXattr(string path, string xattr, ref byte[] buf) => ErrorNumber.NotSupported; + + /// + public ErrorNumber ReadLink(string path, out string dest) + { + dest = null; + + return ErrorNumber.NotSupported; + } + + /// + public FileSystem Metadata { get; private set; } + + /// + public IEnumerable<(string name, Type type, string description)> SupportedOptions => + Array.Empty<(string name, Type type, string description)>(); + + /// + public Dictionary Namespaces => null; + #endregion + + static Dictionary GetDefaultOptions() => new() + { + { + "debug", false.ToString() + } + }; } \ No newline at end of file diff --git a/Aaru.Filesystems/CBM/Dir.cs b/Aaru.Filesystems/CBM/Dir.cs new file mode 100644 index 000000000..87fa13308 --- /dev/null +++ b/Aaru.Filesystems/CBM/Dir.cs @@ -0,0 +1,100 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : CBM.cs +// Author(s) : Natalia Portillo +// +// Component : Commodore file system plugin. +// +// --[ 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-2023 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Linq; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Filesystems; + +/// +/// Implements detection of the filesystem used in 8-bit Commodore microcomputers +public sealed partial class CBM +{ +#region IReadOnlyFilesystem Members + + /// + public ErrorNumber OpenDir(string path, out IDirNode node) + { + node = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(!string.IsNullOrEmpty(path) && string.Compare(path, "/", StringComparison.OrdinalIgnoreCase) != 0) + return ErrorNumber.NotSupported; + + var contents = _cache.Keys.ToList(); + + contents.Sort(); + + node = new CbmDirNode + { + Path = path, Position = 0, Contents = contents.ToArray() + }; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber CloseDir(IDirNode node) + { + if(node is not CbmDirNode mynode) + return ErrorNumber.InvalidArgument; + + mynode.Position = -1; + mynode.Contents = null; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadDir(IDirNode node, out string filename) + { + filename = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(node is not CbmDirNode mynode) + return ErrorNumber.InvalidArgument; + + if(mynode.Position < 0) + return ErrorNumber.InvalidArgument; + + if(mynode.Position >= mynode.Contents.Length) + return ErrorNumber.NoError; + + filename = mynode.Contents[mynode.Position++]; + + return ErrorNumber.NoError; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Filesystems/CBM/File.cs b/Aaru.Filesystems/CBM/File.cs new file mode 100644 index 000000000..01dec4c0f --- /dev/null +++ b/Aaru.Filesystems/CBM/File.cs @@ -0,0 +1,178 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : CBM.cs +// Author(s) : Natalia Portillo +// +// Component : Commodore file system plugin. +// +// --[ 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-2023 Natalia Portillo +// ****************************************************************************/ + +using System; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Filesystems; + +/// +/// Implements detection of the filesystem used in 8-bit Commodore microcomputers +public sealed partial class CBM +{ +#region IReadOnlyFilesystem Members + + /// + public ErrorNumber GetAttributes(string path, out FileAttributes attributes) + { + attributes = new FileAttributes(); + + if(!_mounted) + return ErrorNumber.AccessDenied; + + string[] pathElements = path.Split(new[] + { + '/' + }, StringSplitOptions.RemoveEmptyEntries); + + if(pathElements.Length != 1) + return ErrorNumber.NotSupported; + + string filename = pathElements[0].ToUpperInvariant(); + + if(!_cache.TryGetValue(filename, out CachedFile file)) + return ErrorNumber.NoSuchFile; + + attributes = file.attributes; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber Stat(string path, out FileEntryInfo stat) + { + stat = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + string[] pathElements = path.Split(new[] + { + '/' + }, StringSplitOptions.RemoveEmptyEntries); + + if(pathElements.Length != 1) + return ErrorNumber.NotSupported; + + string filename = pathElements[0].ToUpperInvariant(); + + if(filename.Length > 14) + return ErrorNumber.NameTooLong; + + if(!_cache.TryGetValue(filename, out CachedFile file)) + return ErrorNumber.NoSuchFile; + + stat = new FileEntryInfo + { + Attributes = file.attributes, + BlockSize = 256, + Length = (long)file.length, + Blocks = file.blocks, + Inode = file.id, + Links = 1 + }; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber OpenFile(string path, out IFileNode node) + { + node = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + string[] pathElements = path.Split(new[] + { + '/' + }, StringSplitOptions.RemoveEmptyEntries); + + if(pathElements.Length != 1) + return ErrorNumber.NotSupported; + + string filename = pathElements[0].ToUpperInvariant(); + + if(filename.Length > 14) + return ErrorNumber.NameTooLong; + + if(!_cache.TryGetValue(filename, out CachedFile file)) + return ErrorNumber.NoSuchFile; + + node = new CbmFileNode + { + Path = path, Length = (long)file.length, Offset = 0, Cache = file.data + }; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber CloseFile(IFileNode node) + { + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(node is not CbmFileNode mynode) + return ErrorNumber.InvalidArgument; + + mynode.Cache = null; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadFile(IFileNode node, long length, byte[] buffer, out long read) + { + read = 0; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + if(buffer is null || buffer.Length < length) + return ErrorNumber.InvalidArgument; + + if(node is not CbmFileNode mynode) + return ErrorNumber.InvalidArgument; + + read = length; + + if(length + mynode.Offset >= mynode.Length) + read = mynode.Length - mynode.Offset; + + Array.Copy(mynode.Cache, mynode.Offset, buffer, 0, read); + + mynode.Offset += read; + + return ErrorNumber.NoError; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Filesystems/CBM/Structs.cs b/Aaru.Filesystems/CBM/Structs.cs index d65c26645..8e62ff492 100644 --- a/Aaru.Filesystems/CBM/Structs.cs +++ b/Aaru.Filesystems/CBM/Structs.cs @@ -27,6 +27,8 @@ // ****************************************************************************/ using System.Runtime.InteropServices; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; namespace Aaru.Filesystems; @@ -81,6 +83,81 @@ public sealed partial class CBM #endregion +#region Nested type: CachedFile + + struct CachedFile + { + public byte[] data; + public ulong length; + public FileAttributes attributes; + public int blocks; + public ulong id; + } + +#endregion + +#region Nested type: CbmDirNode + + sealed class CbmDirNode : IDirNode + { + internal string[] Contents; + internal int Position; + + #region IDirNode Members + + /// + public string Path { get; init; } + + #endregion + } + +#endregion + +#region Nested type: CbmFileNode + + sealed class CbmFileNode : IFileNode + { + internal byte[] Cache; + + #region IFileNode Members + + /// + public string Path { get; init; } + + /// + public long Length { get; init; } + + /// + public long Offset { get; set; } + + #endregion + } + +#endregion + +#region Nested type: DirectoryEntry + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + readonly struct DirectoryEntry + { + public readonly byte nextDirBlockTrack; + public readonly byte nextDirBlockSector; + public readonly byte fileType; + public readonly byte firstFileBlockTrack; + public readonly byte firstFileBlockSector; + /// Filename + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public readonly byte[] name; + public readonly byte firstSideBlockTrack; + public readonly byte firstSideBlockSector; + public readonly ulong unused; + public readonly byte replacementTrack; + public readonly byte replacementSector; + public readonly short blocks; + } + +#endregion + #region Nested type: Header [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/Aaru.Filesystems/CBM/Super.cs b/Aaru.Filesystems/CBM/Super.cs new file mode 100644 index 000000000..674debd5f --- /dev/null +++ b/Aaru.Filesystems/CBM/Super.cs @@ -0,0 +1,328 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : CBM.cs +// Author(s) : Natalia Portillo +// +// Component : Commodore file system plugin. +// +// --[ 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-2023 Natalia Portillo +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Console; +using Aaru.Helpers; +using Claunia.Encoding; +using Encoding = System.Text.Encoding; +using FileAttributes = Aaru.CommonTypes.Structs.FileAttributes; +using FileSystemInfo = Aaru.CommonTypes.Structs.FileSystemInfo; +using Partition = Aaru.CommonTypes.Partition; + +namespace Aaru.Filesystems; + +/// +/// Implements detection of the filesystem used in 8-bit Commodore microcomputers +public sealed partial class CBM +{ +#region IReadOnlyFilesystem Members + + /// + public ErrorNumber Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding, + Dictionary options, string @namespace) + { + if(partition.Start > 0) + return ErrorNumber.InvalidArgument; + + if(imagePlugin.Info.SectorSize != 256) + return ErrorNumber.InvalidArgument; + + if(imagePlugin.Info.Sectors != 683 && + imagePlugin.Info.Sectors != 768 && + imagePlugin.Info.Sectors != 1366 && + imagePlugin.Info.Sectors != 3200) + return ErrorNumber.InvalidArgument; + + options ??= GetDefaultOptions(); + + if(options.TryGetValue("debug", out string debugString)) + bool.TryParse(debugString, out _debug); + + encoding = new PETSCII(); + byte[] sector; + ulong rootLba; + string volumeName = null; + Metadata = new FileSystem + { + Type = FS_TYPE, Clusters = imagePlugin.Info.Sectors, ClusterSize = 256 + }; + uint serial; + // Commodore 1581 + if(imagePlugin.Info.Sectors == 3200) + { + ErrorNumber errno = imagePlugin.ReadSector(1560, out _diskHeader); + + if(errno != ErrorNumber.NoError) + return errno; + + Header cbmHdr = Marshal.ByteArrayToStructureLittleEndian
(_diskHeader); + + if(cbmHdr.diskDosVersion != 0x44 || cbmHdr is not { dosVersion: 0x33, diskVersion: 0x44 }) + return ErrorNumber.InvalidArgument; + + _bam = new byte[512]; + // Got to first BAM sector + errno = imagePlugin.ReadSector(1561, out sector); + + if(errno != ErrorNumber.NoError) + return errno; + + Array.Copy(sector, 0, _bam, 0, 256); + + if(_bam[0] > 0) + { + // Got to next (and last) BAM sector + errno = imagePlugin.ReadSector((ulong)((_bam[0] - 1) * 40), out sector); + + if(errno != ErrorNumber.NoError) + return errno; + + Array.Copy(sector, 0, _bam, 256, 256); + } + + if(cbmHdr.directoryTrack == 0) + return ErrorNumber.InvalidArgument; + + rootLba = (ulong)((cbmHdr.directoryTrack - 1) * 40 + cbmHdr.directorySector - 1); + serial = cbmHdr.diskId; + Metadata.VolumeName = StringHandlers.CToString(cbmHdr.name, encoding); + Metadata.VolumeSerial = $"{cbmHdr.diskId}"; + } + else + { + ErrorNumber errno = imagePlugin.ReadSector(357, out _bam); + + if(errno != ErrorNumber.NoError) + return errno; + + BAM cbmBam = Marshal.ByteArrayToStructureLittleEndian(_bam); + + if(cbmBam is not ({ dosVersion: 0x41, doubleSided : 0x00 or 0x80 } + and { unused1 : 0x00, directoryTrack: 0x12 })) + return ErrorNumber.InvalidArgument; + + rootLba = (ulong)((cbmBam.directoryTrack - 1) * 40 + cbmBam.directorySector - 1); + serial = cbmBam.diskId; + + Metadata.VolumeName = StringHandlers.CToString(cbmBam.name, encoding); + Metadata.VolumeSerial = $"{cbmBam.diskId}"; + } + + if(rootLba >= imagePlugin.Info.Sectors) + return ErrorNumber.IllegalSeek; + + ulong nextLba = rootLba; + var rootMs = new MemoryStream(); + var relativeFileWarningShown = false; + + do + { + ErrorNumber errno = imagePlugin.ReadSector(nextLba, out sector); + + if(errno != ErrorNumber.NoError) + return errno; + + rootMs.Write(sector, 0, 256); + + if(sector[0] == 0) + break; + + nextLba = (ulong)((sector[0] - 1) * 40 + sector[1] - 1); + } while(nextLba > 0); + + _root = rootMs.ToArray(); + + _statfs = new FileSystemInfo + { + Blocks = imagePlugin.Info.Sectors, + FilenameLength = 14, + Files = 0, + FreeBlocks = + imagePlugin.Info.Sectors - + (ulong)(_diskHeader?.Length ?? 0 / 256 - _bam.Length / 256 - _root.Length / 256), + FreeFiles = (ulong)(_root.Length / 32), + Id = new FileSystemId + { + Serial32 = serial, IsInt = true + }, + PluginId = Id, + Type = "CBMFS" + }; + + // As this filesystem comes in (by nowadays standards) very small sizes, we can cache all files + _cache = new Dictionary(); + var offset = 0; + ulong fileId = 0; + + if(_debug) + { + // Root + _cache.Add("$", new CachedFile + { + attributes = FileAttributes.Directory | FileAttributes.Hidden | FileAttributes.System, + length = (ulong)_root.Length, + data = _root, + blocks = _root.Length / 256, + id = fileId++ + }); + + // BAM + _cache.Add("$BAM", new CachedFile + { + attributes = FileAttributes.File | FileAttributes.Hidden | FileAttributes.System, + length = (ulong)_bam.Length, + data = _bam, + blocks = _bam.Length / 256, + id = fileId++ + }); + + _statfs.Files += 2; + + // 1581 disk header + if(_diskHeader != null) + { + _cache.Add("$DISK_HEADER", new CachedFile + { + attributes = FileAttributes.File | FileAttributes.Hidden | FileAttributes.System, + length = (ulong)_diskHeader.Length, + data = _diskHeader, + blocks = _diskHeader.Length / 256, + id = fileId++ + }); + _statfs.Files++; + } + } + + while(offset < _root.Length) + { + DirectoryEntry dirEntry = Marshal.ByteArrayToStructureLittleEndian(_root, offset, 32); + + if(dirEntry.fileType == 0) + { + offset += 32; + continue; + } + + _statfs.Files++; + _statfs.FreeFiles--; + + for(var i = 0; i < dirEntry.name.Length; i++) + { + if(dirEntry.name[i] == 0xA0) + dirEntry.name[i] = 0; + } + + string name = StringHandlers.CToString(dirEntry.name, encoding); + + if((dirEntry.fileType & 0x07) == 4 && !relativeFileWarningShown) + { + AaruConsole.WriteLine(Localization.CBM_Mount_REL_file_warning); + relativeFileWarningShown = true; + } + + var data = new MemoryStream(); + + nextLba = (ulong)((dirEntry.firstFileBlockTrack - 1) * 40 + dirEntry.firstFileBlockSector - 1); + + _statfs.FreeBlocks -= (ulong)dirEntry.blocks; + + while(dirEntry.blocks > 0) + { + if(dirEntry.firstFileBlockTrack == 0) + break; + + ErrorNumber errno = imagePlugin.ReadSector(nextLba, out sector); + if(errno != ErrorNumber.NoError) + break; + + data.Write(sector, 2, 254); + + if(sector[0] == 0) + break; + + nextLba = (ulong)((sector[0] - 1) * 40 + sector[1] - 1); + } + + FileAttributes attributes = FileAttributes.File; + + if((dirEntry.fileType & 0x80) != 0x80) + attributes |= FileAttributes.Open; + if((dirEntry.fileType & 0x40) > 0) + attributes |= FileAttributes.ReadOnly; + if((dirEntry.fileType & 7) == 2) + attributes |= FileAttributes.Executable; + + _cache.Add(name, new CachedFile + { + attributes = attributes, + length = (ulong)data.Length, + data = data.ToArray(), + blocks = dirEntry.blocks, + id = fileId++ + }); + + offset += 32; + } + + _mounted = true; + return ErrorNumber.NoError; + } + + /// + public ErrorNumber Unmount() + { + if(!_mounted) + return ErrorNumber.AccessDenied; + + _mounted = false; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber StatFs(out FileSystemInfo stat) + { + stat = null; + + if(!_mounted) + return ErrorNumber.AccessDenied; + + stat = _statfs.ShallowCopy(); + + return ErrorNumber.NoError; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Filesystems/Localization/Localization.Designer.cs b/Aaru.Filesystems/Localization/Localization.Designer.cs index de09465bc..50998d04c 100644 --- a/Aaru.Filesystems/Localization/Localization.Designer.cs +++ b/Aaru.Filesystems/Localization/Localization.Designer.cs @@ -2517,6 +2517,15 @@ namespace Aaru.Filesystems { } } + /// + /// Looks up a localized string similar to This disk contains a relative file. These have not been fully tested, please open a bug report and include this disk image.. + /// + internal static string CBM_Mount_REL_file_warning { + get { + return ResourceManager.GetString("CBM_Mount_REL_file_warning", resourceCulture); + } + } + /// /// Looks up a localized string similar to Commodore file system. /// diff --git a/Aaru.Filesystems/Localization/Localization.es.resx b/Aaru.Filesystems/Localization/Localization.es.resx index 2545e6419..34b78fdda 100644 --- a/Aaru.Filesystems/Localization/Localization.es.resx +++ b/Aaru.Filesystems/Localization/Localization.es.resx @@ -3680,4 +3680,7 @@ 512 bytes por bloque + + Este disco contiene un fichero relativo. Estos no han sido totalmente probados, por favor abra un reporte de errores y adjunte esta imagen de disco. + \ No newline at end of file diff --git a/Aaru.Filesystems/Localization/Localization.resx b/Aaru.Filesystems/Localization/Localization.resx index c23a711fe..a8b4a9784 100644 --- a/Aaru.Filesystems/Localization/Localization.resx +++ b/Aaru.Filesystems/Localization/Localization.resx @@ -3689,4 +3689,7 @@ {0} = Unknown data type {1} + + This disk contains a relative file. These have not been fully tested, please open a bug report and include this disk image. + \ No newline at end of file diff --git a/README.md b/README.md index a584bbd6b..322d9726c 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ Supported disk image formats (read and write) Supported disk image formats (read-only) ======================================== + * Symbian Installation File (.SIS) Supported partitioning schemes @@ -165,6 +166,7 @@ Supported file systems for read-only operations * Apple Lisa file system * Apple Macintosh File System (MFS) * CD-i file system +* Commodore 1540/1541/1571/1581 filesystems * CP/M file system * High Sierra Format * ISO9660, including Apple, Amiga, Rock Ridge, Joliet and Romeo extensions @@ -192,7 +194,6 @@ Supported file systems for identification and information only * BSD Unix File System 2 (UFS2) * B-tree file system (btrfs) * Coherent UNIX file system -* Commodore 1540/1541/1571/1581 filesystems * Cram file system * DEC Files-11 (only checked with On Disk Structure 2, ODS-2) * DEC RT-11 file system