Esempio n. 1
0
        /// <inheritdoc />
        /// <summary>Gets information about the mounted volume.</summary>
        /// <param name="stat">Information about the mounted volume.</param>
        public Errno StatFs(out FileSystemInfo stat)
        {
            stat = null;

            if (!_mounted)
            {
                return(Errno.AccessDenied);
            }

            stat = _cpmStat;

            return(Errno.NoError);
        }
Esempio n. 2
0
        public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
                           Dictionary <string, string> options, string @namespace)
        {
            _device  = imagePlugin;
            Encoding = encoding ?? Encoding.GetEncoding("IBM437");

            options ??= GetDefaultOptions();

            if (options.TryGetValue("debug", out string debugString))
            {
                bool.TryParse(debugString, out _debug);
            }

            // As the identification is so complex, just call Identify() and relay on its findings
            if (!Identify(_device, partition) ||
                !_cpmFound ||
                _workingDefinition == null ||
                _dpb == null)
            {
                return(Errno.InvalidArgument);
            }

            // Build the software interleaving sector mask
            if (_workingDefinition.sides == 1)
            {
                _sectorMask = new int[_workingDefinition.side1.sectorIds.Length];

                for (int m = 0; m < _sectorMask.Length; m++)
                {
                    _sectorMask[m] = _workingDefinition.side1.sectorIds[m] - _workingDefinition.side1.sectorIds[0];
                }
            }
            else
            {
                // Head changes after every track
                if (string.Compare(_workingDefinition.order, "SIDES", StringComparison.InvariantCultureIgnoreCase) == 0)
                {
                    _sectorMask = new int[_workingDefinition.side1.sectorIds.Length +
                                          _workingDefinition.side2.sectorIds.Length];

                    for (int m = 0; m < _workingDefinition.side1.sectorIds.Length; m++)
                    {
                        _sectorMask[m] = _workingDefinition.side1.sectorIds[m] - _workingDefinition.side1.sectorIds[0];
                    }

                    // Skip first track (first side)
                    for (int m = 0; m < _workingDefinition.side2.sectorIds.Length; m++)
                    {
                        _sectorMask[m + _workingDefinition.side1.sectorIds.Length] =
                            (_workingDefinition.side2.sectorIds[m] - _workingDefinition.side2.sectorIds[0]) +
                            _workingDefinition.side1.sectorIds.Length;
                    }
                }

                // Head changes after whole side
                else if (string.Compare(_workingDefinition.order, "CYLINDERS",
                                        StringComparison.InvariantCultureIgnoreCase) == 0)
                {
                    for (int m = 0; m < _workingDefinition.side1.sectorIds.Length; m++)
                    {
                        _sectorMask[m] = _workingDefinition.side1.sectorIds[m] - _workingDefinition.side1.sectorIds[0];
                    }

                    // Skip first track (first side) and first track (second side)
                    for (int m = 0; m < _workingDefinition.side1.sectorIds.Length; m++)
                    {
                        _sectorMask[m + _workingDefinition.side1.sectorIds.Length] =
                            (_workingDefinition.side1.sectorIds[m] - _workingDefinition.side1.sectorIds[0]) +
                            _workingDefinition.side1.sectorIds.Length + _workingDefinition.side2.sectorIds.Length;
                    }

                    // TODO: Implement CYLINDERS ordering
                    AaruConsole.DebugWriteLine("CP/M Plugin", "CYLINDERS ordering not yet implemented.");

                    return(Errno.NotImplemented);
                }

                // TODO: Implement COLUMBIA ordering
                else if (string.Compare(_workingDefinition.order, "COLUMBIA",
                                        StringComparison.InvariantCultureIgnoreCase) == 0)
                {
                    AaruConsole.DebugWriteLine("CP/M Plugin",
                                               "Don't know how to handle COLUMBIA ordering, not proceeding with this definition.");

                    return(Errno.NotImplemented);
                }

                // TODO: Implement EAGLE ordering
                else if (string.Compare(_workingDefinition.order, "EAGLE",
                                        StringComparison.InvariantCultureIgnoreCase) == 0)
                {
                    AaruConsole.DebugWriteLine("CP/M Plugin",
                                               "Don't know how to handle EAGLE ordering, not proceeding with this definition.");

                    return(Errno.NotImplemented);
                }
                else
                {
                    AaruConsole.DebugWriteLine("CP/M Plugin",
                                               "Unknown order type \"{0}\", not proceeding with this definition.",
                                               _workingDefinition.order);

                    return(Errno.NotSupported);
                }
            }

            // Deinterleave whole volume
            Dictionary <ulong, byte[]> deinterleavedSectors = new Dictionary <ulong, byte[]>();

            if (_workingDefinition.sides == 1 ||
                string.Compare(_workingDefinition.order, "SIDES", StringComparison.InvariantCultureIgnoreCase) == 0)
            {
                AaruConsole.DebugWriteLine("CP/M Plugin", "Deinterleaving whole volume.");

                for (int p = 0; p <= (int)(partition.End - partition.Start); p++)
                {
                    byte[] readSector =
                        _device.ReadSector((ulong)((int)partition.Start +
                                                   ((p / _sectorMask.Length) * _sectorMask.Length) +
                                                   _sectorMask[p % _sectorMask.Length]));

                    if (_workingDefinition.complement)
                    {
                        for (int b = 0; b < readSector.Length; b++)
                        {
                            readSector[b] = (byte)(~readSector[b] & 0xFF);
                        }
                    }

                    deinterleavedSectors.Add((ulong)p, readSector);
                }
            }

            int   blockSize       = 128 << _dpb.bsh;
            var   blockMs         = new MemoryStream();
            ulong blockNo         = 0;
            int   sectorsPerBlock = 0;
            Dictionary <ulong, byte[]> allocationBlocks = new Dictionary <ulong, byte[]>();

            AaruConsole.DebugWriteLine("CP/M Plugin", "Creating allocation blocks.");

            // For each volume sector
            for (ulong a = 0; a < (ulong)deinterleavedSectors.Count; a++)
            {
                deinterleavedSectors.TryGetValue(a, out byte[] sector);

                // May it happen? Just in case, CP/M blocks are smaller than physical sectors
                if (sector.Length > blockSize)
                {
                    for (int i = 0; i < sector.Length / blockSize; i++)
                    {
                        byte[] tmp = new byte[blockSize];
                        Array.Copy(sector, blockSize * i, tmp, 0, blockSize);
                        allocationBlocks.Add(blockNo++, tmp);
                    }
                }

                // CP/M blocks are larger than physical sectors
                else if (sector.Length < blockSize)
                {
                    blockMs.Write(sector, 0, sector.Length);
                    sectorsPerBlock++;

                    if (sectorsPerBlock != blockSize / sector.Length)
                    {
                        continue;
                    }

                    allocationBlocks.Add(blockNo++, blockMs.ToArray());
                    sectorsPerBlock = 0;
                    blockMs         = new MemoryStream();
                }

                // CP/M blocks are same size than physical sectors
                else
                {
                    allocationBlocks.Add(blockNo++, sector);
                }
            }

            AaruConsole.DebugWriteLine("CP/M Plugin", "Reading directory.");

            int dirOff;
            int dirSectors = ((_dpb.drm + 1) * 32) / _workingDefinition.bytesPerSector;

            if (_workingDefinition.sofs > 0)
            {
                dirOff = _workingDefinition.sofs;
            }
            else
            {
                dirOff = _workingDefinition.ofs * _workingDefinition.sectorsPerTrack;
            }

            // Read the whole directory blocks
            var dirMs = new MemoryStream();

            for (int d = 0; d < dirSectors; d++)
            {
                deinterleavedSectors.TryGetValue((ulong)(d + dirOff), out byte[] sector);
                dirMs.Write(sector, 0, sector.Length);
            }

            byte[] directory = dirMs.ToArray();

            if (directory == null)
            {
                return(Errno.InvalidArgument);
            }

            int    dirCnt = 0;
            string file1  = null;
            string file2  = null;
            string file3  = null;

            Dictionary <string, Dictionary <int, List <ushort> > > fileExtents =
                new Dictionary <string, Dictionary <int, List <ushort> > >();

            _statCache = new Dictionary <string, FileEntryInfo>();
            _cpmStat   = new FileSystemInfo();
            bool atime = false;

            _dirList           = new List <string>();
            _labelCreationDate = null;
            _labelUpdateDate   = null;
            _passwordCache     = new Dictionary <string, byte[]>();

            AaruConsole.DebugWriteLine("CP/M Plugin", "Traversing directory.");

            // For each directory entry
            for (int dOff = 0; dOff < directory.Length; dOff += 32)
            {
                // Describes a file (does not support PDOS entries with user >= 16, because they're identical to password entries
                if ((directory[dOff] & 0x7F) < 0x10)
                {
                    if (allocationBlocks.Count > 256)
                    {
                        DirectoryEntry16 entry =
                            Marshal.ByteArrayToStructureLittleEndian <DirectoryEntry16>(directory, dOff, 32);

                        bool hidden = (entry.statusUser & 0x80) == 0x80;
                        bool rdOnly = (entry.filename[0] & 0x80) == 0x80 || (entry.extension[0] & 0x80) == 0x80;
                        bool system = (entry.filename[1] & 0x80) == 0x80 || (entry.extension[2] & 0x80) == 0x80;

                        //bool backed = (entry.filename[3] & 0x80) == 0x80 || (entry.extension[3] & 0x80) == 0x80;
                        int user = entry.statusUser & 0x0F;

                        bool validEntry = true;

                        for (int i = 0; i < 8; i++)
                        {
                            entry.filename[i] &= 0x7F;
                            validEntry        &= entry.filename[i] >= 0x20;
                        }

                        for (int i = 0; i < 3; i++)
                        {
                            entry.extension[i] &= 0x7F;
                            validEntry         &= entry.extension[i] >= 0x20;
                        }

                        if (!validEntry)
                        {
                            continue;
                        }

                        string filename  = Encoding.ASCII.GetString(entry.filename).Trim();
                        string extension = Encoding.ASCII.GetString(entry.extension).Trim();

                        // If user is != 0, append user to name to have identical filenames
                        if (user > 0)
                        {
                            filename = $"{user:X1}:{filename}";
                        }

                        if (!string.IsNullOrEmpty(extension))
                        {
                            filename = filename + "." + extension;
                        }

                        int entryNo = ((32 * entry.extentCounter) + entry.extentCounterHigh) / (_dpb.exm + 1);

                        // Do we have a stat for the file already?
                        if (_statCache.TryGetValue(filename, out FileEntryInfo fInfo))
                        {
                            _statCache.Remove(filename);
                        }
                        else
                        {
                            fInfo = new FileEntryInfo
                            {
                                Attributes = new FileAttributes()
                            }
                        };

                        // And any extent?
                        if (fileExtents.TryGetValue(filename, out Dictionary <int, List <ushort> > extentBlocks))
                        {
                            fileExtents.Remove(filename);
                        }
                        else
                        {
                            extentBlocks = new Dictionary <int, List <ushort> >();
                        }

                        // Do we already have this extent? Should never happen
                        if (extentBlocks.TryGetValue(entryNo, out List <ushort> blocks))
                        {
                            extentBlocks.Remove(entryNo);
                        }
                        else
                        {
                            blocks = new List <ushort>();
                        }

                        // Attributes
                        if (hidden)
                        {
                            fInfo.Attributes |= FileAttributes.Hidden;
                        }

                        if (rdOnly)
                        {
                            fInfo.Attributes |= FileAttributes.ReadOnly;
                        }

                        if (system)
                        {
                            fInfo.Attributes |= FileAttributes.System;
                        }

                        // Supposedly there is a value in the directory entry telling how many blocks are designated in
                        // this entry. However some implementations tend to do whatever they wish, but none will ever
                        // allocate block 0 for a file because that's where the directory resides.
                        // There is also a field telling how many bytes are used in the last block, but its meaning is
                        // non-standard so we must ignore it.
                        foreach (ushort blk in entry.allocations.Where(blk => !blocks.Contains(blk) && blk != 0))
                        {
                            blocks.Add(blk);
                        }

                        // Save the file
                        fInfo.UID = (ulong)user;
                        extentBlocks.Add(entryNo, blocks);
                        fileExtents.Add(filename, extentBlocks);
                        _statCache.Add(filename, fInfo);

                        // Add the file to the directory listing
                        if (!_dirList.Contains(filename))
                        {
                            _dirList.Add(filename);
                        }

                        // Count entries 3 by 3 for timestamps
                        switch (dirCnt % 3)
                        {
                        case 0:
                            file1 = filename;

                            break;

                        case 1:
                            file2 = filename;

                            break;

                        case 2:
                            file3 = filename;

                            break;
                        }

                        dirCnt++;
                    }
                    else
                    {
                        DirectoryEntry entry =
                            Marshal.ByteArrayToStructureLittleEndian <DirectoryEntry>(directory, dOff, 32);

                        bool hidden = (entry.statusUser & 0x80) == 0x80;
                        bool rdOnly = (entry.filename[0] & 0x80) == 0x80 || (entry.extension[0] & 0x80) == 0x80;
                        bool system = (entry.filename[1] & 0x80) == 0x80 || (entry.extension[2] & 0x80) == 0x80;

                        //bool backed = (entry.filename[3] & 0x80) == 0x80 || (entry.extension[3] & 0x80) == 0x80;
                        int user = entry.statusUser & 0x0F;

                        bool validEntry = true;

                        for (int i = 0; i < 8; i++)
                        {
                            entry.filename[i] &= 0x7F;
                            validEntry        &= entry.filename[i] >= 0x20;
                        }

                        for (int i = 0; i < 3; i++)
                        {
                            entry.extension[i] &= 0x7F;
                            validEntry         &= entry.extension[i] >= 0x20;
                        }

                        if (!validEntry)
                        {
                            continue;
                        }

                        string filename  = Encoding.ASCII.GetString(entry.filename).Trim();
                        string extension = Encoding.ASCII.GetString(entry.extension).Trim();

                        // If user is != 0, append user to name to have identical filenames
                        if (user > 0)
                        {
                            filename = $"{user:X1}:{filename}";
                        }

                        if (!string.IsNullOrEmpty(extension))
                        {
                            filename = filename + "." + extension;
                        }

                        int entryNo = ((32 * entry.extentCounterHigh) + entry.extentCounter) / (_dpb.exm + 1);

                        // Do we have a stat for the file already?
                        if (_statCache.TryGetValue(filename, out FileEntryInfo fInfo))
                        {
                            _statCache.Remove(filename);
                        }
                        else
                        {
                            fInfo = new FileEntryInfo
                            {
                                Attributes = new FileAttributes()
                            }
                        };

                        // And any extent?
                        if (fileExtents.TryGetValue(filename, out Dictionary <int, List <ushort> > extentBlocks))
                        {
                            fileExtents.Remove(filename);
                        }
                        else
                        {
                            extentBlocks = new Dictionary <int, List <ushort> >();
                        }

                        // Do we already have this extent? Should never happen
                        if (extentBlocks.TryGetValue(entryNo, out List <ushort> blocks))
                        {
                            extentBlocks.Remove(entryNo);
                        }
                        else
                        {
                            blocks = new List <ushort>();
                        }

                        // Attributes
                        if (hidden)
                        {
                            fInfo.Attributes |= FileAttributes.Hidden;
                        }

                        if (rdOnly)
                        {
                            fInfo.Attributes |= FileAttributes.ReadOnly;
                        }

                        if (system)
                        {
                            fInfo.Attributes |= FileAttributes.System;
                        }

                        // Supposedly there is a value in the directory entry telling how many blocks are designated in
                        // this entry. However some implementations tend to do whatever they wish, but none will ever
                        // allocate block 0 for a file because that's where the directory resides.
                        // There is also a field telling how many bytes are used in the last block, but its meaning is
                        // non-standard so we must ignore it.
                        foreach (ushort blk in entry.allocations.Where(blk => !blocks.Contains(blk) && blk != 0))
                        {
                            blocks.Add(blk);
                        }

                        // Save the file
                        fInfo.UID = (ulong)user;
                        extentBlocks.Add(entryNo, blocks);
                        fileExtents.Add(filename, extentBlocks);
                        _statCache.Add(filename, fInfo);

                        // Add the file to the directory listing
                        if (!_dirList.Contains(filename))
                        {
                            _dirList.Add(filename);
                        }

                        // Count entries 3 by 3 for timestamps
                        switch (dirCnt % 3)
                        {
                        case 0:
                            file1 = filename;

                            break;

                        case 1:
                            file2 = filename;

                            break;

                        case 2:
                            file3 = filename;

                            break;
                        }

                        dirCnt++;
                    }
                }

                // A password entry (or a file entry in PDOS, but this does not handle that case)
                else if ((directory[dOff] & 0x7F) >= 0x10 &&
                         (directory[dOff] & 0x7F) < 0x20)
                {
                    PasswordEntry entry = Marshal.ByteArrayToStructureLittleEndian <PasswordEntry>(directory, dOff, 32);

                    int user = entry.userNumber & 0x0F;

                    for (int i = 0; i < 8; i++)
                    {
                        entry.filename[i] &= 0x7F;
                    }

                    for (int i = 0; i < 3; i++)
                    {
                        entry.extension[i] &= 0x7F;
                    }

                    string filename  = Encoding.ASCII.GetString(entry.filename).Trim();
                    string extension = Encoding.ASCII.GetString(entry.extension).Trim();

                    // If user is != 0, append user to name to have identical filenames
                    if (user > 0)
                    {
                        filename = $"{user:X1}:{filename}";
                    }

                    if (!string.IsNullOrEmpty(extension))
                    {
                        filename = filename + "." + extension;
                    }

                    // Do not repeat passwords
                    if (_passwordCache.ContainsKey(filename))
                    {
                        _passwordCache.Remove(filename);
                    }

                    // Copy whole password entry
                    byte[] tmp = new byte[32];
                    Array.Copy(directory, dOff, tmp, 0, 32);
                    _passwordCache.Add(filename, tmp);

                    // Count entries 3 by 3 for timestamps
                    switch (dirCnt % 3)
                    {
                    case 0:
                        file1 = filename;

                        break;

                    case 1:
                        file2 = filename;

                        break;

                    case 2:
                        file3 = filename;

                        break;
                    }

                    dirCnt++;
                }

                // Volume label and password entry. Volume password is ignored.
                else
                {
                    switch (directory[dOff] & 0x7F)
                    {
                    case 0x20:
                        LabelEntry labelEntry =
                            Marshal.ByteArrayToStructureLittleEndian <LabelEntry>(directory, dOff, 32);

                        // The volume label defines if one of the fields in CP/M 3 timestamp is a creation or an
                        // access time
                        atime |= (labelEntry.flags & 0x40) == 0x40;

                        _label             = Encoding.ASCII.GetString(directory, dOff + 1, 11).Trim();
                        _labelCreationDate = new byte[4];
                        _labelUpdateDate   = new byte[4];
                        Array.Copy(directory, dOff + 24, _labelCreationDate, 0, 4);
                        Array.Copy(directory, dOff + 28, _labelUpdateDate, 0, 4);

                        // Count entries 3 by 3 for timestamps
                        switch (dirCnt % 3)
                        {
                        case 0:
                            file1 = null;

                            break;

                        case 1:
                            file2 = null;

                            break;

                        case 2:
                            file3 = null;

                            break;
                        }

                        dirCnt++;

                        break;

                    case 0x21:
                        if (directory[dOff + 10] == 0x00 &&
                            directory[dOff + 20] == 0x00 &&
                            directory[dOff + 30] == 0x00 &&
                            directory[dOff + 31] == 0x00)
                        {
                            DateEntry dateEntry =
                                Marshal.ByteArrayToStructureLittleEndian <DateEntry>(directory, dOff, 32);

                            FileEntryInfo fInfo;

                            // Entry contains timestamps for last 3 entries, whatever the kind they are.
                            if (!string.IsNullOrEmpty(file1))
                            {
                                if (_statCache.TryGetValue(file1, out fInfo))
                                {
                                    _statCache.Remove(file1);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                if (atime)
                                {
                                    fInfo.AccessTime = DateHandlers.CpmToDateTime(dateEntry.date1);
                                }
                                else
                                {
                                    fInfo.CreationTime = DateHandlers.CpmToDateTime(dateEntry.date1);
                                }

                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(dateEntry.date2);

                                _statCache.Add(file1, fInfo);
                            }

                            if (!string.IsNullOrEmpty(file2))
                            {
                                if (_statCache.TryGetValue(file2, out fInfo))
                                {
                                    _statCache.Remove(file2);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                if (atime)
                                {
                                    fInfo.AccessTime = DateHandlers.CpmToDateTime(dateEntry.date3);
                                }
                                else
                                {
                                    fInfo.CreationTime = DateHandlers.CpmToDateTime(dateEntry.date3);
                                }

                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(dateEntry.date4);

                                _statCache.Add(file2, fInfo);
                            }

                            if (!string.IsNullOrEmpty(file3))
                            {
                                if (_statCache.TryGetValue(file3, out fInfo))
                                {
                                    _statCache.Remove(file3);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                if (atime)
                                {
                                    fInfo.AccessTime = DateHandlers.CpmToDateTime(dateEntry.date5);
                                }
                                else
                                {
                                    fInfo.CreationTime = DateHandlers.CpmToDateTime(dateEntry.date5);
                                }

                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(dateEntry.date6);

                                _statCache.Add(file3, fInfo);
                            }

                            file1  = null;
                            file2  = null;
                            file3  = null;
                            dirCnt = 0;
                        }

                        // However, if this byte is 0, timestamp is in Z80DOS or DOS+ format
                        else if (directory[dOff + 1] == 0x00)
                        {
                            TrdPartyDateEntry trdPartyDateEntry =
                                Marshal.ByteArrayToStructureLittleEndian <TrdPartyDateEntry>(directory, dOff, 32);

                            FileEntryInfo fInfo;

                            // Entry contains timestamps for last 3 entries, whatever the kind they are.
                            if (!string.IsNullOrEmpty(file1))
                            {
                                if (_statCache.TryGetValue(file1, out fInfo))
                                {
                                    _statCache.Remove(file1);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                byte[] ctime = new byte[4];
                                ctime[0] = trdPartyDateEntry.create1[0];
                                ctime[1] = trdPartyDateEntry.create1[1];

                                fInfo.AccessTime    = DateHandlers.CpmToDateTime(trdPartyDateEntry.access1);
                                fInfo.CreationTime  = DateHandlers.CpmToDateTime(ctime);
                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(trdPartyDateEntry.modify1);

                                _statCache.Add(file1, fInfo);
                            }

                            if (!string.IsNullOrEmpty(file2))
                            {
                                if (_statCache.TryGetValue(file2, out fInfo))
                                {
                                    _statCache.Remove(file2);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                byte[] ctime = new byte[4];
                                ctime[0] = trdPartyDateEntry.create2[0];
                                ctime[1] = trdPartyDateEntry.create2[1];

                                fInfo.AccessTime    = DateHandlers.CpmToDateTime(trdPartyDateEntry.access2);
                                fInfo.CreationTime  = DateHandlers.CpmToDateTime(ctime);
                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(trdPartyDateEntry.modify2);

                                _statCache.Add(file2, fInfo);
                            }

                            if (!string.IsNullOrEmpty(file3))
                            {
                                if (_statCache.TryGetValue(file1, out fInfo))
                                {
                                    _statCache.Remove(file3);
                                }
                                else
                                {
                                    fInfo = new FileEntryInfo();
                                }

                                byte[] ctime = new byte[4];
                                ctime[0] = trdPartyDateEntry.create3[0];
                                ctime[1] = trdPartyDateEntry.create3[1];

                                fInfo.AccessTime    = DateHandlers.CpmToDateTime(trdPartyDateEntry.access3);
                                fInfo.CreationTime  = DateHandlers.CpmToDateTime(ctime);
                                fInfo.LastWriteTime = DateHandlers.CpmToDateTime(trdPartyDateEntry.modify3);

                                _statCache.Add(file3, fInfo);
                            }

                            file1  = null;
                            file2  = null;
                            file3  = null;
                            dirCnt = 0;
                        }

                        break;
                    }
                }
            }

            // Cache all files. As CP/M maximum volume size is 8 Mib
            // this should not be a problem
            AaruConsole.DebugWriteLine("CP/M Plugin", "Reading files.");
            long usedBlocks = 0;

            _fileCache = new Dictionary <string, byte[]>();

            foreach (string filename in _dirList)
            {
                var fileMs = new MemoryStream();

                if (_statCache.TryGetValue(filename, out FileEntryInfo fInfo))
                {
                    _statCache.Remove(filename);
                }

                fInfo.Blocks = 0;

                if (fileExtents.TryGetValue(filename, out Dictionary <int, List <ushort> > extents))
                {
                    for (int ex = 0; ex < extents.Count; ex++)
                    {
                        if (!extents.TryGetValue(ex, out List <ushort> alBlks))
                        {
                            continue;
                        }

                        foreach (ushort alBlk in alBlks)
                        {
                            allocationBlocks.TryGetValue(alBlk, out byte[] blk);
                            fileMs.Write(blk, 0, blk.Length);
                            fInfo.Blocks++;
                        }
                    }
                }

                // If you insist to call CP/M "extent based"
                fInfo.Attributes |= FileAttributes.Extents;
                fInfo.BlockSize   = blockSize;
                fInfo.Length      = fileMs.Length;
                _cpmStat.Files++;
                usedBlocks += fInfo.Blocks;

                _statCache.Add(filename, fInfo);
                _fileCache.Add(filename, fileMs.ToArray());
            }

            _decodedPasswordCache = new Dictionary <string, byte[]>();

            // For each stored password, store a decoded version of it
            if (_passwordCache.Count > 0)
            {
                foreach (KeyValuePair <string, byte[]> kvp in _passwordCache)
                {
                    byte[] tmp = new byte[8];
                    Array.Copy(kvp.Value, 16, tmp, 0, 8);

                    for (int t = 0; t < 8; t++)
                    {
                        tmp[t] ^= kvp.Value[13];
                    }

                    _decodedPasswordCache.Add(kvp.Key, tmp);
                }
            }

            // Generate statfs.
            _cpmStat.Blocks         = (ulong)(_dpb.dsm + 1);
            _cpmStat.FilenameLength = 11;
            _cpmStat.Files          = (ulong)_fileCache.Count;
            _cpmStat.FreeBlocks     = _cpmStat.Blocks - (ulong)usedBlocks;
            _cpmStat.PluginId       = Id;
            _cpmStat.Type           = "CP/M filesystem";

            // Generate XML info
            XmlFsType = new FileSystemType
            {
                Clusters              = _cpmStat.Blocks,
                ClusterSize           = (uint)blockSize,
                Files                 = (ulong)_fileCache.Count,
                FilesSpecified        = true,
                FreeClusters          = _cpmStat.FreeBlocks,
                FreeClustersSpecified = true,
                Type = "CP/M filesystem"
            };

            if (_labelCreationDate != null)
            {
                XmlFsType.CreationDate          = DateHandlers.CpmToDateTime(_labelCreationDate);
                XmlFsType.CreationDateSpecified = true;
            }

            if (_labelUpdateDate != null)
            {
                XmlFsType.ModificationDate          = DateHandlers.CpmToDateTime(_labelUpdateDate);
                XmlFsType.ModificationDateSpecified = true;
            }

            if (!string.IsNullOrEmpty(_label))
            {
                XmlFsType.VolumeName = _label;
            }

            _mounted = true;

            return(Errno.NoError);
        }
Esempio n. 3
0
        /// <inheritdoc />
        public Errno Mount(IMediaImage imagePlugin, Partition partition, Encoding encoding,
                           Dictionary <string, string> options, string @namespace)
        {
            XmlFsType = new FileSystemType();

            options ??= GetDefaultOptions();

            if (options.TryGetValue("debug", out string debugString))
            {
                bool.TryParse(debugString, out _debug);
            }

            // Default namespace
            @namespace ??= "ecs";

            switch (@namespace.ToLowerInvariant())
            {
            case "dos":
                _namespace = Namespace.Dos;

                break;

            case "nt":
                _namespace = Namespace.Nt;

                break;

            case "os2":
                _namespace = Namespace.Os2;

                break;

            case "ecs":
                _namespace = Namespace.Ecs;

                break;

            case "lfn":
                _namespace = Namespace.Lfn;

                break;

            case "human":
                _namespace = Namespace.Human;

                break;

            default: return(Errno.InvalidArgument);
            }

            AaruConsole.DebugWriteLine("FAT plugin", "Reading BPB");

            uint sectorsPerBpb = imagePlugin.Info.SectorSize < 512 ? 512 / imagePlugin.Info.SectorSize : 1;

            byte[] bpbSector = imagePlugin.ReadSectors(0 + partition.Start, sectorsPerBpb);

            BpbKind bpbKind = DetectBpbKind(bpbSector, imagePlugin, partition, out BiosParameterBlockEbpb fakeBpb,
                                            out HumanParameterBlock humanBpb, out AtariParameterBlock atariBpb,
                                            out byte minBootNearJump, out bool andosOemCorrect, out bool bootable);

            _fat12             = false;
            _fat16             = false;
            _fat32             = false;
            _useFirstFat       = true;
            XmlFsType.Bootable = bootable;

            _statfs = new FileSystemInfo
            {
                FilenameLength = 11,
                Files          = 0, // Requires traversing all directories
                FreeFiles      = 0,
                PluginId       = Id,
                FreeBlocks     = 0 // Requires traversing the FAT
            };

            // This is needed because for FAT16, GEMDOS increases bytes per sector count instead of using big_sectors field.
            uint sectorsPerRealSector = 1;

            // This is needed because some OSes don't put volume label as first entry in the root directory
            uint sectorsForRootDirectory = 0;
            uint rootDirectoryCluster    = 0;

            Encoding = encoding ?? (bpbKind == BpbKind.Human ? Encoding.GetEncoding("shift_jis")
                                        : Encoding.GetEncoding("IBM437"));

            switch (bpbKind)
            {
            case BpbKind.DecRainbow:
            case BpbKind.Hardcoded:
            case BpbKind.Msx:
            case BpbKind.Apricot:
                _fat12 = true;

                break;

            case BpbKind.ShortFat32:
            case BpbKind.LongFat32:
            {
                _fat32 = true;

                Fat32ParameterBlock fat32Bpb =
                    Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlock>(bpbSector);

                Fat32ParameterBlockShort shortFat32Bpb =
                    Marshal.ByteArrayToStructureLittleEndian <Fat32ParameterBlockShort>(bpbSector);

                rootDirectoryCluster = fat32Bpb.root_cluster;

                // This is to support FAT partitions on hybrid ISO/USB images
                if (imagePlugin.Info.XmlMediaType == XmlMediaType.OpticalDisc)
                {
                    fat32Bpb.bps       *= 4;
                    fat32Bpb.spc       /= 4;
                    fat32Bpb.big_spfat /= 4;
                    fat32Bpb.hsectors  /= 4;
                    fat32Bpb.sptrk     /= 4;
                }

                XmlFsType.Type = fat32Bpb.version != 0 ? "FAT+" : "FAT32";

                if (fat32Bpb.oem_name != null &&
                    (fat32Bpb.oem_name[5] != 0x49 || fat32Bpb.oem_name[6] != 0x48 || fat32Bpb.oem_name[7] != 0x43))
                {
                    XmlFsType.SystemIdentifier = StringHandlers.CToString(fat32Bpb.oem_name);
                }

                _sectorsPerCluster    = fat32Bpb.spc;
                XmlFsType.ClusterSize = (uint)(fat32Bpb.bps * fat32Bpb.spc);
                _reservedSectors      = fat32Bpb.rsectors;

                if (fat32Bpb.big_sectors == 0 &&
                    fat32Bpb.signature == 0x28)
                {
                    XmlFsType.Clusters = shortFat32Bpb.huge_sectors / shortFat32Bpb.spc;
                }
                else if (fat32Bpb.sectors == 0)
                {
                    XmlFsType.Clusters = fat32Bpb.big_sectors / fat32Bpb.spc;
                }
                else
                {
                    XmlFsType.Clusters = (ulong)(fat32Bpb.sectors / fat32Bpb.spc);
                }

                _sectorsPerFat         = fat32Bpb.big_spfat;
                XmlFsType.VolumeSerial = $"{fat32Bpb.serial_no:X8}";

                _statfs.Id = new FileSystemId
                {
                    IsInt    = true,
                    Serial32 = fat32Bpb.serial_no
                };

                if ((fat32Bpb.flags & 0xF8) == 0x00)
                {
                    if ((fat32Bpb.flags & 0x01) == 0x01)
                    {
                        XmlFsType.Dirty = true;
                    }
                }

                if ((fat32Bpb.mirror_flags & 0x80) == 0x80)
                {
                    _useFirstFat = (fat32Bpb.mirror_flags & 0xF) != 1;
                }

                if (fat32Bpb.signature == 0x29)
                {
                    XmlFsType.VolumeName = StringHandlers.SpacePaddedToString(fat32Bpb.volume_label, Encoding);
                    XmlFsType.VolumeName = XmlFsType.VolumeName?.Replace("\0", "");
                }

                // Check that jumps to a correct boot code position and has boot signature set.
                // This will mean that the volume will boot, even if just to say "this is not bootable change disk"......
                XmlFsType.Bootable =
                    (fat32Bpb.jump[0] == 0xEB && fat32Bpb.jump[1] >= minBootNearJump && fat32Bpb.jump[1] < 0x80) ||
                    (fat32Bpb.jump[0] == 0xE9 && fat32Bpb.jump.Length >= 3 &&
                     BitConverter.ToUInt16(fat32Bpb.jump, 1) >= minBootNearJump &&
                     BitConverter.ToUInt16(fat32Bpb.jump, 1) <= 0x1FC);

                sectorsPerRealSector = fat32Bpb.bps / imagePlugin.Info.SectorSize;
                _sectorsPerCluster  *= sectorsPerRealSector;

                // First root directory sector
                _firstClusterSector =
                    ((ulong)((fat32Bpb.big_spfat * fat32Bpb.fats_no) + fat32Bpb.rsectors) * sectorsPerRealSector) -
                    (2 * _sectorsPerCluster);

                if (fat32Bpb.fsinfo_sector + partition.Start <= partition.End)
                {
                    byte[] fsinfoSector = imagePlugin.ReadSector(fat32Bpb.fsinfo_sector + partition.Start);

                    FsInfoSector fsInfo = Marshal.ByteArrayToStructureLittleEndian <FsInfoSector>(fsinfoSector);

                    if (fsInfo.signature1 == FSINFO_SIGNATURE1 &&
                        fsInfo.signature2 == FSINFO_SIGNATURE2 &&
                        fsInfo.signature3 == FSINFO_SIGNATURE3)
                    {
                        if (fsInfo.free_clusters < 0xFFFFFFFF)
                        {
                            XmlFsType.FreeClusters          = fsInfo.free_clusters;
                            XmlFsType.FreeClustersSpecified = true;
                        }
                    }
                }

                break;
            }

            // Some fields could overflow fake BPB, those will be handled below
            case BpbKind.Atari:
            {
                ushort sum = 0;

                for (int i = 0; i < bpbSector.Length; i += 2)
                {
                    sum += BigEndianBitConverter.ToUInt16(bpbSector, i);
                }

                // TODO: Check this
                if (sum == 0x1234)
                {
                    XmlFsType.Bootable = true;
                }

                // BGM changes the bytes per sector instead of changing the sectors per cluster. Why?! WHY!?
                uint ratio = fakeBpb.bps / imagePlugin.Info.SectorSize;
                fakeBpb.bps         = (ushort)imagePlugin.Info.SectorSize;
                fakeBpb.spc         = (byte)(fakeBpb.spc * ratio);
                fakeBpb.rsectors    = (ushort)(fakeBpb.rsectors * ratio);
                fakeBpb.big_sectors = fakeBpb.sectors * ratio;
                fakeBpb.sectors     = 0;
                fakeBpb.spfat       = (ushort)(fakeBpb.spfat * ratio);
                fakeBpb.sptrk       = (ushort)(fakeBpb.sptrk * ratio);

                break;
            }

            case BpbKind.Human:
                // If not debug set Human68k namespace and ShiftJIS codepage as defaults
                if (!_debug)
                {
                    _namespace = Namespace.Human;
                }

                XmlFsType.Bootable = true;

                break;
            }

            ulong firstRootSector = 0;

            if (!_fat32)
            {
                // This is to support FAT partitions on hybrid ISO/USB images
                if (imagePlugin.Info.XmlMediaType == XmlMediaType.OpticalDisc)
                {
                    fakeBpb.bps      *= 4;
                    fakeBpb.spc      /= 4;
                    fakeBpb.spfat    /= 4;
                    fakeBpb.hsectors /= 4;
                    fakeBpb.sptrk    /= 4;
                    fakeBpb.rsectors /= 4;

                    if (fakeBpb.spc == 0)
                    {
                        fakeBpb.spc = 1;
                    }
                }

                ulong clusters;

                if (bpbKind != BpbKind.Human)
                {
                    int reservedSectors = fakeBpb.rsectors + (fakeBpb.fats_no * fakeBpb.spfat) +
                                          (fakeBpb.root_ent * 32 / fakeBpb.bps);

                    if (fakeBpb.sectors == 0)
                    {
                        clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.big_sectors - reservedSectors
                                               : (fakeBpb.big_sectors - reservedSectors) / fakeBpb.spc);
                    }
                    else
                    {
                        clusters = (ulong)(fakeBpb.spc == 0 ? fakeBpb.sectors - reservedSectors
                                               : (fakeBpb.sectors - reservedSectors) / fakeBpb.spc);
                    }
                }
                else
                {
                    clusters = humanBpb.clusters == 0 ? humanBpb.big_clusters : humanBpb.clusters;
                }

                // This will walk all the FAT entries and check if they're valid FAT12 or FAT16 entries.
                // If the whole table is valid in both senses, it considers the type entry in the BPB.
                // BeOS is known to set the type as FAT16 but treat it as FAT12.
                if (!_fat12 &&
                    !_fat16)
                {
                    if (clusters < 4089)
                    {
                        ushort[] fat12 = new ushort[clusters];

                        _reservedSectors     = fakeBpb.rsectors;
                        sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize;
                        _fatFirstSector      = partition.Start + (_reservedSectors * sectorsPerRealSector);

                        byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, fakeBpb.spfat);

                        int pos = 0;

                        for (int i = 0; i + 3 < fatBytes.Length && pos < fat12.Length; i += 3)
                        {
                            fat12[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);

                            if (pos >= fat12.Length)
                            {
                                break;
                            }

                            fat12[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
                        }

                        bool fat12Valid = fat12[0] >= FAT12_RESERVED && fat12[1] >= FAT12_RESERVED;

                        foreach (ushort entry in fat12)
                        {
                            if (entry >= FAT12_RESERVED ||
                                entry <= clusters)
                            {
                                continue;
                            }

                            fat12Valid = false;

                            break;
                        }

                        ushort[] fat16 = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray();

                        bool fat16Valid = fat16[0] >= FAT16_RESERVED && fat16[1] >= 0x3FF0;

                        foreach (ushort entry in fat16)
                        {
                            if (entry >= FAT16_RESERVED ||
                                entry <= clusters)
                            {
                                continue;
                            }

                            fat16Valid = false;

                            break;
                        }

                        _fat12 = fat12Valid;
                        _fat16 = fat16Valid;

                        // Check BPB type
                        if (_fat12 == _fat16)
                        {
                            _fat12 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT12   ";
                            _fat16 = Encoding.ASCII.GetString(fakeBpb.fs_type) == "FAT16   ";
                        }
                    }
                    else
                    {
                        _fat16 = true;
                    }
                }

                if (_fat12)
                {
                    XmlFsType.Type = "FAT12";
                }
                else if (_fat16)
                {
                    XmlFsType.Type = "FAT16";
                }

                if (bpbKind == BpbKind.Atari)
                {
                    if (atariBpb.serial_no[0] != 0x49 ||
                        atariBpb.serial_no[1] != 0x48 ||
                        atariBpb.serial_no[2] != 0x43)
                    {
                        XmlFsType.VolumeSerial =
                            $"{atariBpb.serial_no[0]:X2}{atariBpb.serial_no[1]:X2}{atariBpb.serial_no[2]:X2}";

                        _statfs.Id = new FileSystemId
                        {
                            IsInt    = true,
                            Serial32 = (uint)((atariBpb.serial_no[0] << 16) + (atariBpb.serial_no[1] << 8) +
                                              atariBpb.serial_no[2])
                        };
                    }

                    XmlFsType.SystemIdentifier = StringHandlers.CToString(atariBpb.oem_name);

                    if (string.IsNullOrEmpty(XmlFsType.SystemIdentifier))
                    {
                        XmlFsType.SystemIdentifier = null;
                    }
                }
                else if (fakeBpb.oem_name != null)
                {
                    if (fakeBpb.oem_name[5] != 0x49 ||
                        fakeBpb.oem_name[6] != 0x48 ||
                        fakeBpb.oem_name[7] != 0x43)
                    {
                        // Later versions of Windows create a DOS 3 BPB without OEM name on 8 sectors/track floppies
                        // OEM ID should be ASCII, otherwise ignore it
                        if (fakeBpb.oem_name[0] >= 0x20 &&
                            fakeBpb.oem_name[0] <= 0x7F &&
                            fakeBpb.oem_name[1] >= 0x20 &&
                            fakeBpb.oem_name[1] <= 0x7F &&
                            fakeBpb.oem_name[2] >= 0x20 &&
                            fakeBpb.oem_name[2] <= 0x7F &&
                            fakeBpb.oem_name[3] >= 0x20 &&
                            fakeBpb.oem_name[3] <= 0x7F &&
                            fakeBpb.oem_name[4] >= 0x20 &&
                            fakeBpb.oem_name[4] <= 0x7F &&
                            fakeBpb.oem_name[5] >= 0x20 &&
                            fakeBpb.oem_name[5] <= 0x7F &&
                            fakeBpb.oem_name[6] >= 0x20 &&
                            fakeBpb.oem_name[6] <= 0x7F &&
                            fakeBpb.oem_name[7] >= 0x20 &&
                            fakeBpb.oem_name[7] <= 0x7F)
                        {
                            XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name);
                        }
                        else if (fakeBpb.oem_name[0] < 0x20 &&
                                 fakeBpb.oem_name[1] >= 0x20 &&
                                 fakeBpb.oem_name[1] <= 0x7F &&
                                 fakeBpb.oem_name[2] >= 0x20 &&
                                 fakeBpb.oem_name[2] <= 0x7F &&
                                 fakeBpb.oem_name[3] >= 0x20 &&
                                 fakeBpb.oem_name[3] <= 0x7F &&
                                 fakeBpb.oem_name[4] >= 0x20 &&
                                 fakeBpb.oem_name[4] <= 0x7F &&
                                 fakeBpb.oem_name[5] >= 0x20 &&
                                 fakeBpb.oem_name[5] <= 0x7F &&
                                 fakeBpb.oem_name[6] >= 0x20 &&
                                 fakeBpb.oem_name[6] <= 0x7F &&
                                 fakeBpb.oem_name[7] >= 0x20 &&
                                 fakeBpb.oem_name[7] <= 0x7F)
                        {
                            XmlFsType.SystemIdentifier = StringHandlers.CToString(fakeBpb.oem_name, Encoding, start: 1);
                        }
                    }

                    if (fakeBpb.signature == 0x28 ||
                        fakeBpb.signature == 0x29)
                    {
                        XmlFsType.VolumeSerial = $"{fakeBpb.serial_no:X8}";

                        _statfs.Id = new FileSystemId
                        {
                            IsInt    = true,
                            Serial32 = fakeBpb.serial_no
                        };
                    }
                }

                XmlFsType.Clusters    = clusters;
                _sectorsPerCluster    = fakeBpb.spc;
                XmlFsType.ClusterSize = (uint)(fakeBpb.bps * fakeBpb.spc);
                _reservedSectors      = fakeBpb.rsectors;
                _sectorsPerFat        = fakeBpb.spfat;

                if (fakeBpb.signature == 0x28 ||
                    fakeBpb.signature == 0x29 ||
                    andosOemCorrect)
                {
                    if ((fakeBpb.flags & 0xF8) == 0x00)
                    {
                        if ((fakeBpb.flags & 0x01) == 0x01)
                        {
                            XmlFsType.Dirty = true;
                        }
                    }

                    if (fakeBpb.signature == 0x29 || andosOemCorrect)
                    {
                        XmlFsType.VolumeName = StringHandlers.SpacePaddedToString(fakeBpb.volume_label, Encoding);
                        XmlFsType.VolumeName = XmlFsType.VolumeName?.Replace("\0", "");
                    }
                }

                // Workaround that PCExchange jumps into "FAT16   "...
                if (XmlFsType.SystemIdentifier == "PCX 2.0 ")
                {
                    fakeBpb.jump[1] += 8;
                }

                // Check that jumps to a correct boot code position and has boot signature set.
                // This will mean that the volume will boot, even if just to say "this is not bootable change disk"......
                if (XmlFsType.Bootable == false &&
                    fakeBpb.jump != null)
                {
                    XmlFsType.Bootable |=
                        (fakeBpb.jump[0] == 0xEB && fakeBpb.jump[1] >= minBootNearJump && fakeBpb.jump[1] < 0x80) ||
                        (fakeBpb.jump[0] == 0xE9 && fakeBpb.jump.Length >= 3 &&
                         BitConverter.ToUInt16(fakeBpb.jump, 1) >= minBootNearJump &&
                         BitConverter.ToUInt16(fakeBpb.jump, 1) <= 0x1FC);
                }

                // First root directory sector
                firstRootSector =
                    ((ulong)((fakeBpb.spfat * fakeBpb.fats_no) + fakeBpb.rsectors) * sectorsPerRealSector) +
                    partition.Start;

                sectorsForRootDirectory = (uint)(fakeBpb.root_ent * 32 / imagePlugin.Info.SectorSize);

                sectorsPerRealSector = fakeBpb.bps / imagePlugin.Info.SectorSize;
                _sectorsPerCluster  *= sectorsPerRealSector;
            }

            _firstClusterSector += partition.Start;

            _image = imagePlugin;

            if (_fat32)
            {
                _fatEntriesPerSector = imagePlugin.Info.SectorSize / 4;
            }
            else if (_fat16)
            {
                _fatEntriesPerSector = imagePlugin.Info.SectorSize / 2;
            }
            else
            {
                _fatEntriesPerSector = imagePlugin.Info.SectorSize * 2 / 3;
            }

            _fatFirstSector = partition.Start + (_reservedSectors * sectorsPerRealSector);

            _rootDirectoryCache = new Dictionary <string, CompleteDirectoryEntry>();
            byte[] rootDirectory;

            if (!_fat32)
            {
                _firstClusterSector = firstRootSector + sectorsForRootDirectory - (_sectorsPerCluster * 2);
                rootDirectory       = imagePlugin.ReadSectors(firstRootSector, sectorsForRootDirectory);

                if (bpbKind == BpbKind.DecRainbow)
                {
                    var rootMs = new MemoryStream();

                    foreach (byte[] tmp in from ulong rootSector in new[]
                    {
                        0x17, 0x19, 0x1B, 0x1D, 0x1E, 0x20
                    } select imagePlugin.ReadSector(rootSector))
                    {
                        rootMs.Write(tmp, 0, tmp.Length);
                    }

                    rootDirectory = rootMs.ToArray();
                }
            }
            else
            {
                if (rootDirectoryCluster == 0)
                {
                    return(Errno.InvalidArgument);
                }

                var    rootMs = new MemoryStream();
                uint[] rootDirectoryClusters = GetClusters(rootDirectoryCluster);

                foreach (byte[] buffer in rootDirectoryClusters.Select(cluster =>
                                                                       imagePlugin.
                                                                       ReadSectors(_firstClusterSector + (cluster * _sectorsPerCluster),
                                                                                   _sectorsPerCluster)))
                {
                    rootMs.Write(buffer, 0, buffer.Length);
                }

                rootDirectory = rootMs.ToArray();

                // OS/2 FAT32.IFS uses LFN instead of .LONGNAME
                if (_namespace == Namespace.Os2)
                {
                    _namespace = Namespace.Lfn;
                }
            }

            if (rootDirectory is null)
            {
                return(Errno.InvalidArgument);
            }

            byte[] lastLfnName     = null;
            byte   lastLfnChecksum = 0;

            for (int i = 0; i < rootDirectory.Length; i += Marshal.SizeOf <DirectoryEntry>())
            {
                DirectoryEntry entry =
                    Marshal.ByteArrayToStructureLittleEndian <DirectoryEntry>(rootDirectory, i,
                                                                              Marshal.SizeOf <DirectoryEntry>());

                if (entry.filename[0] == DIRENT_FINISHED)
                {
                    break;
                }

                if (entry.attributes.HasFlag(FatAttributes.LFN))
                {
                    if (_namespace != Namespace.Lfn &&
                        _namespace != Namespace.Ecs)
                    {
                        continue;
                    }

                    LfnEntry lfnEntry =
                        Marshal.ByteArrayToStructureLittleEndian <LfnEntry>(rootDirectory, i,
                                                                            Marshal.SizeOf <LfnEntry>());

                    int lfnSequence = lfnEntry.sequence & LFN_MASK;

                    if ((lfnEntry.sequence & LFN_ERASED) > 0)
                    {
                        continue;
                    }

                    if ((lfnEntry.sequence & LFN_LAST) > 0)
                    {
                        lastLfnName     = new byte[lfnSequence * 26];
                        lastLfnChecksum = lfnEntry.checksum;
                    }

                    if (lastLfnName is null)
                    {
                        continue;
                    }

                    if (lfnEntry.checksum != lastLfnChecksum)
                    {
                        continue;
                    }

                    lfnSequence--;

                    Array.Copy(lfnEntry.name1, 0, lastLfnName, lfnSequence * 26, 10);
                    Array.Copy(lfnEntry.name2, 0, lastLfnName, (lfnSequence * 26) + 10, 12);
                    Array.Copy(lfnEntry.name3, 0, lastLfnName, (lfnSequence * 26) + 22, 4);

                    continue;
                }

                // Not a correct entry
                if (entry.filename[0] < DIRENT_MIN &&
                    entry.filename[0] != DIRENT_E5)
                {
                    continue;
                }

                // Self
                if (Encoding.GetString(entry.filename).TrimEnd() == ".")
                {
                    continue;
                }

                // Parent
                if (Encoding.GetString(entry.filename).TrimEnd() == "..")
                {
                    continue;
                }

                // Deleted
                if (entry.filename[0] == DIRENT_DELETED)
                {
                    continue;
                }

                string filename;

                if (entry.attributes.HasFlag(FatAttributes.VolumeLabel))
                {
                    byte[] fullname = new byte[11];
                    Array.Copy(entry.filename, 0, fullname, 0, 8);
                    Array.Copy(entry.extension, 0, fullname, 8, 3);
                    string volname = Encoding.GetString(fullname).Trim();

                    if (!string.IsNullOrEmpty(volname))
                    {
                        XmlFsType.VolumeName =
                            entry.caseinfo.HasFlag(CaseInfo.AllLowerCase) && _namespace == Namespace.Nt
                                ? volname.ToLower() : volname;
                    }

                    XmlFsType.VolumeName = XmlFsType.VolumeName?.Replace("\0", "");

                    if (entry.ctime > 0 &&
                        entry.cdate > 0)
                    {
                        XmlFsType.CreationDate = DateHandlers.DosToDateTime(entry.cdate, entry.ctime);

                        if (entry.ctime_ms > 0)
                        {
                            XmlFsType.CreationDate = XmlFsType.CreationDate.AddMilliseconds(entry.ctime_ms * 10);
                        }

                        XmlFsType.CreationDateSpecified = true;
                    }

                    if (entry.mtime > 0 &&
                        entry.mdate > 0)
                    {
                        XmlFsType.ModificationDate          = DateHandlers.DosToDateTime(entry.mdate, entry.mtime);
                        XmlFsType.ModificationDateSpecified = true;
                    }

                    continue;
                }

                var completeEntry = new CompleteDirectoryEntry
                {
                    Dirent = entry
                };

                if ((_namespace == Namespace.Lfn || _namespace == Namespace.Ecs) &&
                    lastLfnName != null)
                {
                    byte calculatedLfnChecksum = LfnChecksum(entry.filename, entry.extension);

                    if (calculatedLfnChecksum == lastLfnChecksum)
                    {
                        filename = StringHandlers.CToString(lastLfnName, Encoding.Unicode, true);

                        completeEntry.Lfn = filename;
                        lastLfnName       = null;
                        lastLfnChecksum   = 0;
                    }
                }

                if (entry.filename[0] == DIRENT_E5)
                {
                    entry.filename[0] = DIRENT_DELETED;
                }

                string name      = Encoding.GetString(entry.filename).TrimEnd();
                string extension = Encoding.GetString(entry.extension).TrimEnd();

                if (_namespace == Namespace.Nt)
                {
                    if (entry.caseinfo.HasFlag(CaseInfo.LowerCaseExtension))
                    {
                        extension = extension.ToLower(CultureInfo.CurrentCulture);
                    }

                    if (entry.caseinfo.HasFlag(CaseInfo.LowerCaseBasename))
                    {
                        name = name.ToLower(CultureInfo.CurrentCulture);
                    }
                }

                if (extension != "")
                {
                    filename = name + "." + extension;
                }
                else
                {
                    filename = name;
                }

                if (name == "" &&
                    extension == "")
                {
                    AaruConsole.DebugWriteLine("FAT filesystem", "Found empty filename in root directory");

                    if (!_debug ||
                        (entry.size > 0 && entry.start_cluster == 0))
                    {
                        continue; // Skip invalid name
                    }
                    // If debug, add it
                    name = ":{EMPTYNAME}:";

                    // Try to create a unique filename with an extension from 000 to 999
                    for (int uniq = 0; uniq < 1000; uniq++)
                    {
                        extension = $"{uniq:D03}";

                        if (!_rootDirectoryCache.ContainsKey($"{name}.{extension}"))
                        {
                            break;
                        }
                    }

                    // If we couldn't find it, just skip over
                    if (_rootDirectoryCache.ContainsKey($"{name}.{extension}"))
                    {
                        continue;
                    }
                }

                // Atari ST allows slash AND colon so cannot simply substitute one for the other like in Mac filesystems
                filename = filename.Replace('/', '\u2215');

                completeEntry.Shortname = filename;

                if (_namespace == Namespace.Human)
                {
                    HumanDirectoryEntry humanEntry =
                        Marshal.ByteArrayToStructureLittleEndian <HumanDirectoryEntry>(rootDirectory, i,
                                                                                       Marshal.SizeOf <HumanDirectoryEntry>());

                    completeEntry.HumanDirent = humanEntry;

                    name      = StringHandlers.CToString(humanEntry.name1, Encoding).TrimEnd();
                    extension = StringHandlers.CToString(humanEntry.extension, Encoding).TrimEnd();
                    string name2 = StringHandlers.CToString(humanEntry.name2, Encoding).TrimEnd();

                    if (extension != "")
                    {
                        filename = name + name2 + "." + extension;
                    }
                    else
                    {
                        filename = name + name2;
                    }

                    completeEntry.HumanName = filename;
                }

                if (!_fat32 &&
                    filename == "EA DATA. SF")
                {
                    _eaDirEntry     = entry;
                    lastLfnName     = null;
                    lastLfnChecksum = 0;

                    if (_debug)
                    {
                        _rootDirectoryCache[completeEntry.ToString()] = completeEntry;
                    }

                    continue;
                }

                _rootDirectoryCache[completeEntry.ToString()] = completeEntry;
                lastLfnName     = null;
                lastLfnChecksum = 0;
            }

            XmlFsType.VolumeName = XmlFsType.VolumeName?.Trim();
            _statfs.Blocks       = XmlFsType.Clusters;

            switch (bpbKind)
            {
            case BpbKind.Hardcoded:
                _statfs.Type = $"Microsoft FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.Atari:
                _statfs.Type = $"Atari FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.Msx:
                _statfs.Type = $"MSX FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.Dos2:
            case BpbKind.Dos3:
            case BpbKind.Dos32:
            case BpbKind.Dos33:
            case BpbKind.ShortExtended:
            case BpbKind.Extended:
                _statfs.Type = $"Microsoft FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.ShortFat32:
            case BpbKind.LongFat32:
                _statfs.Type = XmlFsType.Type == "FAT+" ? "FAT+" : "Microsoft FAT32";

                break;

            case BpbKind.Andos:
                _statfs.Type = $"ANDOS FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.Apricot:
                _statfs.Type = $"Apricot FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.DecRainbow:
                _statfs.Type = $"DEC FAT{(_fat16 ? "16" : "12")}";

                break;

            case BpbKind.Human:
                _statfs.Type = $"Human68k FAT{(_fat16 ? "16" : "12")}";

                break;

            default: throw new ArgumentOutOfRangeException();
            }

            _bytesPerCluster = _sectorsPerCluster * imagePlugin.Info.SectorSize;

            ushort[] _firstFatEntries  = new ushort[_statfs.Blocks];
            ushort[] _secondFatEntries = new ushort[_statfs.Blocks];
            bool     _firstFatValid    = true;
            bool     _secondFatValid   = true;

            if (_fat12)
            {
                AaruConsole.DebugWriteLine("FAT plugin", "Reading FAT12");

                byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat);

                int pos = 0;

                for (int i = 0; i + 3 < fatBytes.Length && pos < _firstFatEntries.Length; i += 3)
                {
                    _firstFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);

                    if (pos >= _firstFatEntries.Length)
                    {
                        break;
                    }

                    _firstFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
                }

                fatBytes = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat);

                _fatEntries = new ushort[_statfs.Blocks];

                pos = 0;

                for (int i = 0; i + 3 < fatBytes.Length && pos < _secondFatEntries.Length; i += 3)
                {
                    _secondFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF) << 8) + fatBytes[i + 0]);

                    if (pos >= _secondFatEntries.Length)
                    {
                        break;
                    }

                    _secondFatEntries[pos++] = (ushort)(((fatBytes[i + 1] & 0xF0) >> 4) + (fatBytes[i + 2] << 4));
                }

                foreach (ushort entry in _firstFatEntries)
                {
                    if (entry >= FAT12_RESERVED ||
                        entry <= _statfs.Blocks)
                    {
                        continue;
                    }

                    _firstFatValid = false;

                    break;
                }

                foreach (ushort entry in _secondFatEntries)
                {
                    if (entry >= FAT12_RESERVED ||
                        entry <= _statfs.Blocks)
                    {
                        continue;
                    }

                    _secondFatValid = false;

                    break;
                }

                if (_firstFatValid == _secondFatValid)
                {
                    _fatEntries = _useFirstFat ? _firstFatEntries : _secondFatEntries;
                }
                else if (_firstFatValid)
                {
                    _fatEntries = _firstFatEntries;
                }
                else
                {
                    _fatEntries = _secondFatEntries;
                }
            }
            else if (_fat16)
            {
                AaruConsole.DebugWriteLine("FAT plugin", "Reading FAT16");

                byte[] fatBytes = imagePlugin.ReadSectors(_fatFirstSector, _sectorsPerFat);

                AaruConsole.DebugWriteLine("FAT plugin", "Casting FAT");
                _firstFatEntries = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray();

                fatBytes = imagePlugin.ReadSectors(_fatFirstSector + _sectorsPerFat, _sectorsPerFat);

                AaruConsole.DebugWriteLine("FAT plugin", "Casting FAT");
                _secondFatEntries = MemoryMarshal.Cast <byte, ushort>(fatBytes).ToArray();

                foreach (ushort entry in _firstFatEntries)
                {
                    if (entry >= FAT16_RESERVED ||
                        entry <= _statfs.Blocks)
                    {
                        continue;
                    }

                    _firstFatValid = false;

                    break;
                }

                foreach (ushort entry in _secondFatEntries)
                {
                    if (entry >= FAT16_RESERVED ||
                        entry <= _statfs.Blocks)
                    {
                        continue;
                    }

                    _secondFatValid = false;

                    break;
                }

                if (_firstFatValid == _secondFatValid)
                {
                    _fatEntries = _useFirstFat ? _firstFatEntries : _secondFatEntries;
                }
                else if (_firstFatValid)
                {
                    _fatEntries = _firstFatEntries;
                }
                else
                {
                    _fatEntries = _secondFatEntries;
                }
            }

            // TODO: Check how this affects international filenames
            _cultureInfo    = new CultureInfo("en-US", false);
            _directoryCache = new Dictionary <string, Dictionary <string, CompleteDirectoryEntry> >();

            // Check it is really an OS/2 EA file
            if (_eaDirEntry.start_cluster != 0)
            {
                CacheEaData();
                ushort eamagic = BitConverter.ToUInt16(_cachedEaData, 0);

                if (eamagic != EADATA_MAGIC)
                {
                    _eaDirEntry   = new DirectoryEntry();
                    _cachedEaData = null;
                }
                else
                {
                    _eaCache = new Dictionary <string, Dictionary <string, byte[]> >();
                }
            }
            else if (_fat32)
            {
                _eaCache = new Dictionary <string, Dictionary <string, byte[]> >();
            }

            // Check OS/2 .LONGNAME
            if (_eaCache != null &&
                (_namespace == Namespace.Os2 || _namespace == Namespace.Ecs) &&
                !_fat32)
            {
                List <KeyValuePair <string, CompleteDirectoryEntry> > rootFilesWithEas =
                    _rootDirectoryCache.Where(t => t.Value.Dirent.ea_handle != 0).ToList();

                foreach (KeyValuePair <string, CompleteDirectoryEntry> fileWithEa in rootFilesWithEas)
                {
                    Dictionary <string, byte[]> eas = GetEas(fileWithEa.Value.Dirent.ea_handle);

                    if (eas is null)
                    {
                        continue;
                    }

                    if (!eas.TryGetValue("com.microsoft.os2.longname", out byte[] longnameEa))