/* * CiderPress * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. * See the file LICENSE for distribution terms. */ /* * Implementation of DiskFSProDOS class. * * We currently only allow one fork to be open at a time, and each file may * only be opened once. * * BUG: does not keep VolumeUsage up to date. */ #include "StdAfx.h" #include "DiskImgPriv.h" // disable Y2K+ dates when testing w/ProSel-16 vol rep (newer ProSel is OK) //#define OLD_PRODOS_DATES #if defined(OLD_PRODOS_DATES) && !(defined(_DEBUG)) # error "don't set OLD_PRODOS_DATES for production" #endif /* * =========================================================================== * DiskFSProDOS * =========================================================================== */ const int kBlkSize = 512; const int kVolHeaderBlock = 2; // block where Volume Header resides const int kFormatVolDirNumBlocks = 4; // #of volume header blocks for new volumes const int kMinReasonableBlocks = 16; // min size for ProDOS volume const int kExpectedBitmapStart = 6; // block# where vol bitmap should start const int kMaxCatalogIterations = 1024; // theoretical max is 32768? const int kMaxDirectoryDepth = 64; // not sure what ProDOS limit is const int kEntriesPerBlock = 0x0d; // expected value for entries per blk const int kEntryLength = 0x27; // expected value for dir entry len const int kTypeDIR = 0x0f; /* * Directory header. All fields not marked as "only for subdirs" also apply * to the volume directory header. */ typedef struct DiskFSProDOS::DirHeader { uint8_t storageType; char dirName[A2FileProDOS::kMaxFileName+1]; DiskFSProDOS::ProDate createWhen; uint8_t version; uint8_t minVersion; uint8_t access; uint8_t entryLength; uint8_t entriesPerBlock; uint16_t fileCount; /* the rest are only for subdirs */ uint16_t parentPointer; uint8_t parentEntry; uint8_t parentEntryLength; } DirHeader; /* * See if this looks like a ProDOS volume. * * We test a few fields in the volume directory header for validity. */ static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; int volDirEntryLength; int volDirEntriesPerBlock; dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder, DiskImg::kSectorOrderProDOS); if (dierr != kDIErrNone) goto bail; volDirEntryLength = blkBuf[0x23]; volDirEntriesPerBlock = blkBuf[0x24]; if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) || !((blkBuf[0x04] & 0xf0) == 0xf0) || !((blkBuf[0x04] & 0x0f) != 0) || !(volDirEntryLength * volDirEntriesPerBlock <= kBlkSize) || !(blkBuf[0x05] >= 'A' && blkBuf[0x05] <= 'Z') || 0) { dierr = kDIErrFilesystemNotFound; goto bail; } bail: return dierr; } /* * Test to see if the image is a ProDOS disk. */ /*static*/ DIError DiskFSProDOS::TestFS(DiskImg* pImg, DiskImg::SectorOrder* pOrder, DiskImg::FSFormat* pFormat, FSLeniency leniency) { DiskImg::SectorOrder ordering[DiskImg::kSectorOrderMax]; DiskImg::GetSectorOrderArray(ordering, *pOrder); for (int i = 0; i < DiskImg::kSectorOrderMax; i++) { if (ordering[i] == DiskImg::kSectorOrderUnknown) continue; if (TestImage(pImg, ordering[i]) == kDIErrNone) { *pOrder = ordering[i]; *pFormat = DiskImg::kFormatProDOS; return kDIErrNone; } } LOGI(" ProDOS didn't find valid FS"); return kDIErrFilesystemNotFound; } /* * Get things rolling. * * Since we're assured that this is a valid disk, errors encountered from here * on out must be handled somehow, possibly by claiming that the disk has * no files on it. */ DIError DiskFSProDOS::Initialize(InitMode initMode) { DIError dierr = kDIErrNone; char msg[kMaxVolumeName + 32]; fDiskIsGood = false; // hosed until proven innocent fEarlyDamage = false; /* * NOTE: we'd probably be better off with fTotalBlocks, since that's how * big the disk *thinks* it is, especially on a CFFA or MacPart subvol. * However, we know that the image block count is the absolute maximum, * so while it may not be a tight bound it is an upper bound. */ fVolumeUsage.Create(fpImg->GetNumBlocks()); dierr = LoadVolHeader(); if (dierr != kDIErrNone) goto bail; DumpVolHeader(); dierr = ScanVolBitmap(); if (dierr != kDIErrNone) goto bail; if (initMode == kInitHeaderOnly) { LOGI(" ProDOS - headerOnly set, skipping file load"); goto bail; } sprintf(msg, "Scanning %s", fVolumeName); if (!fpImg->UpdateScanProgress(msg)) { LOGI(" ProDOS cancelled by user"); dierr = kDIErrCancelled; goto bail; } /* volume dir is guaranteed to come first; if not, we need a lookup func */ A2FileProDOS* pVolumeDir; pVolumeDir = (A2FileProDOS*) GetNextFile(NULL); dierr = RecursiveDirAdd(pVolumeDir, kVolHeaderBlock, "", 0); if (dierr != kDIErrNone) { LOGI(" ProDOS RecursiveDirAdd failed"); goto bail; } sprintf(msg, "Processing %s", fVolumeName); if (!fpImg->UpdateScanProgress(msg)) { LOGI(" ProDOS cancelled by user"); dierr = kDIErrCancelled; goto bail; } dierr = ScanFileUsage(); if (dierr != kDIErrNone) { if (dierr == kDIErrCancelled) goto bail; /* this might not be fatal; just means that *some* files are bad */ LOGI("WARNING: ScanFileUsage returned err=%d", dierr); dierr = kDIErrNone; fpImg->AddNote(DiskImg::kNoteWarning, "Some errors were encountered while scanning files."); fEarlyDamage = true; // make sure we know it's damaged } fDiskIsGood = CheckDiskIsGood(); if (fScanForSubVolumes != kScanSubDisabled) (void) ScanForSubVolumes(); if (fpImg->GetNumBlocks() <= 1600) fVolumeUsage.Dump(); // A2File* pFile; // pFile = GetNextFile(NULL); // while (pFile != NULL) { // pFile->Dump(); // pFile = GetNextFile(pFile); // } bail: return dierr; } /* * Read some interesting fields from the volume header. * * The "test" function verified certain things, e.g. the storage type * is $f and the volume name length is nonzero. */ DIError DiskFSProDOS::LoadVolHeader(void) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; int nameLen; dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf); if (dierr != kDIErrNone) goto bail; //fPrevBlock = GetShortLE(&blkBuf[0x00]); //fNextBlock = GetShortLE(&blkBuf[0x02]); nameLen = blkBuf[0x04] & 0x0f; memcpy(fVolumeName, &blkBuf[0x05], nameLen); fVolumeName[nameLen] = '\0'; // 0x14-15 reserved // undocumented: GS/OS writes the modification date to 0x16-19 fModWhen = GetLongLE(&blkBuf[0x16]); // undocumented: GS/OS uses 0x1a-1b for lower-case handling (see below) fCreateWhen = GetLongLE(&blkBuf[0x1c]); //fVersion = blkBuf[0x20]; if (blkBuf[0x21] != 0) { /* * We don't care about the MIN_VERSION field, but it looks like GS/OS * rejects anything with a nonzero value here. We want to add a note * about it. */ fpImg->AddNote(DiskImg::kNoteInfo, "Volume header has nonzero min_version; could confuse GS/OS."); } fAccess = blkBuf[0x22]; //fEntryLength = blkBuf[0x23]; //fEntriesPerBlock = blkBuf[0x24]; fVolDirFileCount = GetShortLE(&blkBuf[0x25]); fBitMapPointer = GetShortLE(&blkBuf[0x27]); fTotalBlocks = GetShortLE(&blkBuf[0x29]); if (blkBuf[0x1b] & 0x80) { /* * Handle lower-case conversion; see GS/OS tech note #8. Unlike * filenames, volume names are not allowed to contain spaces. If * they try it we just ignore them. * * Technote 8 doesn't actually talk about volume names. By * experimentation the field was discovered at offset 0x1a from * the start of the block, which is marked as "reserved" in Beneath * Apple ProDOS. */ uint16_t lcFlags = GetShortLE(&blkBuf[0x1a]); GenerateLowerCaseName(fVolumeName, fVolumeName, lcFlags, false); } if (fTotalBlocks <= kVolHeaderBlock) { /* incr to min; don't use max, or bitmap count may be too large */ LOGI(" ProDOS found tiny fTotalBlocks (%d), increasing to minimum", fTotalBlocks); fpImg->AddNote(DiskImg::kNoteWarning, "ProDOS filesystem blockcount (%d) too small, setting to %d.", fTotalBlocks, kMinReasonableBlocks); fTotalBlocks = kMinReasonableBlocks; fEarlyDamage = true; } else if (fTotalBlocks != fpImg->GetNumBlocks()) { if (fTotalBlocks != 65535 || fpImg->GetNumBlocks() != 65536) { LOGI(" ProDOS WARNING: total (%u) != img (%ld)", fTotalBlocks, fpImg->GetNumBlocks()); // could AddNote here, but not really necessary } /* * For safety (esp. vol bitmap read), constrain fTotalBlocks. We might * consider not doing this for ".hdv", which can start small and then * expand as files are added. (Check "fExpanded".) */ if (fTotalBlocks > fpImg->GetNumBlocks()) { fpImg->AddNote(DiskImg::kNoteWarning, "ProDOS filesystem blockcount (%d) exceeds disk image blocks (%ld).", fTotalBlocks, fpImg->GetNumBlocks()); fTotalBlocks = (uint16_t) fpImg->GetNumBlocks(); fEarlyDamage = true; } } /* * Test for funky volume bitmap pointer. Some disks (e.g. /RAM and * ProSel-16) truncate the volume directory to eke a little more storage * out of a disk. There's nothing wrong with that, but we don't want to * try to use a volume bitmap pointer of zero or 0xffff, because it's * probably garbage. */ if (fBitMapPointer != kExpectedBitmapStart) { if (fBitMapPointer <= kVolHeaderBlock || fBitMapPointer > kExpectedBitmapStart) { fpImg->AddNote(DiskImg::kNoteWarning, "Volume bitmap pointer (%d) is probably invalid.", fBitMapPointer); fBitMapPointer = 6; // just fix it and hope for the best fEarlyDamage = true; } else { fpImg->AddNote(DiskImg::kNoteInfo, "Unusual volume bitmap start (%d).", fBitMapPointer); // try it and see } } SetVolumeID(); /* * Create a "magic" directory entry for the volume directory. * * Normally these values are pulled out of the file entry in the parent * directory. Here, we synthesize them from the volume dir header. */ A2FileProDOS* pFile; pFile = new A2FileProDOS(this); if (pFile == NULL) { dierr = kDIErrMalloc; goto bail; } A2FileProDOS::DirEntry* pEntry; pEntry = &pFile->fDirEntry; int foundStorage; foundStorage = (blkBuf[0x04] & 0xf0) >> 4; if (foundStorage != A2FileProDOS::kStorageVolumeDirHeader) { LOGI(" ProDOS WARNING: unexpected vol dir file type %d", pEntry->storageType); /* keep going */ } pEntry->storageType = A2FileProDOS::kStorageVolumeDirHeader; strcpy(pEntry->fileName, fVolumeName); //nameLen = blkBuf[0x04] & 0x0f; //memcpy(pEntry->fileName, &blkBuf[0x05], nameLen); //pEntry->fileName[nameLen] = '\0'; pFile->SetPathName(":", pEntry->fileName); pEntry->fileName[nameLen] = '\0'; pEntry->fileType = kTypeDIR; pEntry->keyPointer = kVolHeaderBlock; dierr = DetermineVolDirLen(GetShortLE(&blkBuf[0x02]), &pEntry->blocksUsed); if (dierr != kDIErrNone) { goto bail; } pEntry->eof = pEntry->blocksUsed * 512; pEntry->createWhen = GetLongLE(&blkBuf[0x1c]); pEntry->version = blkBuf[0x20]; pEntry->minVersion = blkBuf[0x21]; pEntry->access = blkBuf[0x22]; pEntry->auxType = 0; // if (blkBuf[0x20] >= 5) pEntry->modWhen = GetLongLE(&blkBuf[0x16]); pEntry->headerPointer = 0; pFile->fSparseDataEof = pEntry->eof; pFile->fSparseRsrcEof = -1; AddFileToList(pFile); pFile = NULL; bail: delete pFile; return dierr; } DIError DiskFSProDOS::DetermineVolDirLen(uint16_t nextBlock, uint16_t* pBlocksUsed) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; uint16_t blocksUsed = 1; int iterCount = 0; // Traverse the volume directory chain, counting blocks. Normally this will have 4, but // variations are possible. while (nextBlock != 0) { blocksUsed++; if (nextBlock < 2 || nextBlock >= fpImg->GetNumBlocks()) { LOGI(" ProDOS ERROR: invalid volume dir link block %u", nextBlock); dierr = kDIErrInvalidBlock; goto bail; } dierr = fpImg->ReadBlock(nextBlock, blkBuf); if (dierr != kDIErrNone) { goto bail; } nextBlock = GetShortLE(&blkBuf[0x02]); // Watch for infinite loop. iterCount++; if (iterCount > fpImg->GetNumBlocks()) { LOGI(" ProDOS ERROR: infinite vol directory loop found"); dierr = kDIErrDirectoryLoop; goto bail; } } bail: *pBlocksUsed = blocksUsed; return dierr; } /* * Set the volume ID field. */ void DiskFSProDOS::SetVolumeID(void) { sprintf(fVolumeID, "ProDOS /%s", fVolumeName); } /* * Dump what we pulled out of the volume header. */ void DiskFSProDOS::DumpVolHeader(void) { LOGI(" ProDOS volume header for '%s'", fVolumeName); LOGI(" CreateWhen=0x%08x access=0x%02x bitmap=%d totalbl=%d", fCreateWhen, fAccess, fBitMapPointer, fTotalBlocks); time_t when; when = A2FileProDOS::ConvertProDate(fCreateWhen); LOGI(" CreateWhen is %.24s", ctime(&when)); //LOGI(" prev=%d next=%d bitmap=%d total=%d", // fPrevBlock, fNextBlock, fBitMapPointer, fTotalBlocks); //LOGI(" create date=0x%08lx access=0x%02x", fCreateWhen, fAccess); //LOGI(" version=%d minVersion=%d entryLen=%d epb=%d", // fVersion, fMinVersion, fEntryLength, fEntriesPerBlock); //LOGI(" volume dir fileCount=%d", fFileCount); } /* * Load the disk's volume bitmap into the object's "fBlockUseMap" pointer. * * Does not attempt to analyze the data. */ DIError DiskFSProDOS::LoadVolBitmap(void) { DIError dierr = kDIErrNone; int bitBlock, numBlocks; if (fBitMapPointer <= kVolHeaderBlock) return kDIErrBadDiskImage; if (fTotalBlocks <= kVolHeaderBlock) return kDIErrBadDiskImage; /* should not already be allocated */ assert(fBlockUseMap == NULL); delete[] fBlockUseMap; // just in case bitBlock = fBitMapPointer; numBlocks = GetNumBitmapBlocks(); // based on fTotalBlocks assert(numBlocks > 0); fBlockUseMap = new uint8_t[kBlkSize * numBlocks]; if (fBlockUseMap == NULL) return kDIErrMalloc; while (numBlocks--) { dierr = fpImg->ReadBlock(bitBlock + numBlocks, fBlockUseMap + kBlkSize * numBlocks); if (dierr != kDIErrNone) { delete[] fBlockUseMap; fBlockUseMap = NULL; return dierr; } } return kDIErrNone; } /* * Save our copy of the volume bitmap. */ DIError DiskFSProDOS::SaveVolBitmap(void) { DIError dierr = kDIErrNone; int bitBlock, numBlocks; if (fBlockUseMap == NULL) { assert(false); return kDIErrNotReady; } assert(fBitMapPointer > kVolHeaderBlock); assert(fTotalBlocks > kVolHeaderBlock); bitBlock = fBitMapPointer; numBlocks = GetNumBitmapBlocks(); assert(numBlocks > 0); while (numBlocks--) { dierr = fpImg->WriteBlock(bitBlock + numBlocks, fBlockUseMap + kBlkSize * numBlocks); if (dierr != kDIErrNone) return dierr; } return kDIErrNone; } /* * Throw away the volume bitmap, discarding any unsaved changes. * * It's okay to call this if the bitmap isn't loaded. */ void DiskFSProDOS::FreeVolBitmap(void) { delete[] fBlockUseMap; fBlockUseMap = NULL; } /* * Examine the volume bitmap, setting fields in the VolumeUsage map * as appropriate. */ DIError DiskFSProDOS::ScanVolBitmap(void) { DIError dierr; dierr = LoadVolBitmap(); if (dierr != kDIErrNone) { LOGI(" ProDOS failed to load volume bitmap (err=%d)", dierr); return dierr; } assert(fBlockUseMap != NULL); /* mark the boot blocks as system */ SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem); SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem); /* mark the bitmap blocks as system */ int i; for (i = GetNumBitmapBlocks(); i > 0; i--) SetBlockUsage(fBitMapPointer + i -1, VolumeUsage::kChunkPurposeSystem); /* * Set the "isMarkedUsed" flag in VolumeUsage for all used blocks. */ VolumeUsage::ChunkState cstate; long block = 0; long numBytes = (fTotalBlocks + 7) / 8; for (i = 0; i < numBytes; i++) { uint8_t val = fBlockUseMap[i]; for (int j = 0; j < 8; j++) { if (!(val & 0x80)) { /* block is in use, mark it */ if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { assert(false); // keep going, I guess } cstate.isMarkedUsed = true; fVolumeUsage.SetChunkState(block, &cstate); } val <<= 1; block++; if (block >= fTotalBlocks) break; } if (block >= fTotalBlocks) break; } FreeVolBitmap(); return dierr; } /* * Generate an empty block use map. Used by disk formatter. */ DIError DiskFSProDOS::CreateEmptyBlockMap(void) { DIError dierr; /* load from disk; this is just to allocate the data structures */ dierr = LoadVolBitmap(); if (dierr != kDIErrNone) return dierr; /* * Set the bits, block by block. Not the most efficient way, but it's * fast enough, and it exercises the standard set of functions. */ long block; long firstEmpty = kVolHeaderBlock + kFormatVolDirNumBlocks + GetNumBitmapBlocks(); for (block = 0; block < firstEmpty; block++) SetBlockUseEntry(block, true); for ( ; block < fTotalBlocks; block++) SetBlockUseEntry(block, false); dierr = SaveVolBitmap(); FreeVolBitmap(); if (dierr != kDIErrNone) return dierr; return kDIErrNone; } /* * Get the state of an entry in the block use map. * * Returns "true" if it's in use, "false" otherwise. */ bool DiskFSProDOS::GetBlockUseEntry(long block) const { assert(block >= 0 && block < fTotalBlocks); assert(fBlockUseMap != NULL); int offset; uint8_t mask; offset = block / 8; mask = 0x80 >> (block & 0x07); if (fBlockUseMap[offset] & mask) return false; else return true; } /* * Change the state of an entry in the block use map. */ void DiskFSProDOS::SetBlockUseEntry(long block, bool inUse) { assert(block >= 0 && block < fTotalBlocks); assert(fBlockUseMap != NULL); if (block == 0 && !inUse) { // shouldn't happen assert(false); } int offset; uint8_t mask; offset = block / 8; mask = 0x80 >> (block & 0x07); if (!inUse) fBlockUseMap[offset] |= mask; else fBlockUseMap[offset] &= ~mask; } /* * Check for entries in the block use map past the point where they should be. * * Returns "true" if bogus entries were found, "false" if all is well. */ bool DiskFSProDOS::ScanForExtraEntries(void) const { assert(fBlockUseMap != NULL); int offset, endOffset; /* sloppy: we're not checking for excess bits within last byte */ offset = (fTotalBlocks / 8) +1; endOffset = GetNumBitmapBlocks() * kBlkSize; while (offset < endOffset) { if (fBlockUseMap[offset] != 0) { LOGI(" ProDOS found bogus bitmap junk 0x%02x at offset=%d", fBlockUseMap[offset], offset); return true; } offset++; } return false; } /* * Allocate a new block on a ProDOS volume. * * Only touches the in-memory copy. * * Returns the block number (0-65535) on success or -1 on failure. */ long DiskFSProDOS::AllocBlock(void) { assert(fBlockUseMap != NULL); #if 0 // whoa... this is REALLY slow /* * Run through the entire set of blocks until we find one that's not * allocated. We could probably make this faster by scanning bytes and * then shifting bits, but this is easier and fast enough. * * We don't scan block 0 because (a) it should never be available and * (b) it has a special meaning in some circumstances. We could probably * start at kVolHeaderBlock+kVolHeaderNumBlocks. */ long block; for (block = kVolHeaderBlock; block < fTotalBlocks; block++) { if (!GetBlockUseEntry(block)) { SetBlockUseEntry(block, true); return block; } } #endif int offset; int maxOffset = (fTotalBlocks + 7) / 8; for (offset = 0; offset < maxOffset; offset++) { if (fBlockUseMap[offset] != 0) { /* got one, figure out which */ int subBlock = 0; uint8_t uch = fBlockUseMap[offset]; while ((uch & 0x80) == 0) { subBlock++; uch <<= 1; } long block = offset * 8 + subBlock; assert(!GetBlockUseEntry(block)); SetBlockUseEntry(block, true); if (block == 0 || block == 1) { LOGI("PRODOS: GLITCH: rejecting alloc of block 0"); continue; } return block; } } LOGI("ProDOS: NOTE: AllocBlock just failed!"); return -1; } /* * Tally up the number of free blocks. */ DIError DiskFSProDOS::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, int* pUnitSize) const { DIError dierr; long block, freeBlocks; freeBlocks = 0; dierr = const_cast(this)->LoadVolBitmap(); if (dierr != kDIErrNone) return dierr; for (block = 0; block < fTotalBlocks; block++) { if (!GetBlockUseEntry(block)) freeBlocks++; } *pTotalUnits = fTotalBlocks; *pFreeUnits = freeBlocks; *pUnitSize = kBlockSize; const_cast(this)->FreeVolBitmap(); return kDIErrNone; } /* * Update an entry in the VolumeUsage map. * * The VolumeUsage map spans the range of blocks */ void DiskFSProDOS::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose) { VolumeUsage::ChunkState cstate; fVolumeUsage.GetChunkState(block, &cstate); if (cstate.isUsed) { cstate.purpose = VolumeUsage::kChunkPurposeConflict; LOGI(" ProDOS conflicting uses for bl=%ld", block); } else { cstate.isUsed = true; cstate.purpose = purpose; } fVolumeUsage.SetChunkState(block, &cstate); } /* * Pass in the number of the first block of the directory. * * Start with "pParent" set to the magic entry for the volume dir. */ DIError DiskFSProDOS::RecursiveDirAdd(A2File* pParent, uint16_t dirBlock, const char* basePath, int depth) { DIError dierr = kDIErrNone; DirHeader header; uint8_t blkBuf[kBlkSize]; int numEntries, iterations, foundCount; bool first; /* if we get too deep, assume it's a loop */ if (depth > kMaxDirectoryDepth) { dierr = kDIErrDirectoryLoop; goto bail; } if (dirBlock < kVolHeaderBlock || dirBlock >= fpImg->GetNumBlocks()) { LOGI(" ProDOS ERROR: directory block %u out of range", dirBlock); dierr = kDIErrInvalidBlock; goto bail; } numEntries = 1; iterations = 0; foundCount = 0; first = true; while (dirBlock && iterations < kMaxCatalogIterations) { dierr = fpImg->ReadBlock(dirBlock, blkBuf); if (dierr != kDIErrNone) goto bail; if (pParent->IsVolumeDirectory()) SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeVolumeDir); else SetBlockUsage(dirBlock, VolumeUsage::kChunkPurposeSubdir); if (first) { /* this is the directory header entry */ dierr = GetDirHeader(blkBuf, &header); if (dierr != kDIErrNone) goto bail; numEntries = header.fileCount; //LOGI(" ProDOS got dir header numEntries = %d", numEntries); } /* slurp the entries out of this block */ dierr = SlurpEntries(pParent, &header, blkBuf, first, &foundCount, basePath, dirBlock, depth); if (dierr != kDIErrNone) goto bail; dirBlock = GetShortLE(&blkBuf[0x02]); if (dirBlock != 0 && (dirBlock < 2 || dirBlock >= fpImg->GetNumBlocks())) { LOGI(" ProDOS ERROR: invalid dir link block %u in base='%s'", dirBlock, basePath); dierr = kDIErrInvalidBlock; goto bail; } first = false; iterations++; } if (iterations == kMaxCatalogIterations) { LOGI(" ProDOS subdir iteration count exceeded"); dierr = kDIErrDirectoryLoop; goto bail; } if (foundCount != numEntries) { /* not significant; just means somebody isn't updating correctly */ LOGI(" ProDOS WARNING: numEntries=%d foundCount=%d in base='%s'", numEntries, foundCount, basePath); } bail: return dierr; } /* * Slurp the entries out of a single ProDOS directory block. * * Recursively calls RecursiveDirAdd for directories. * * "*pFound" is increased by the number of valid entries found in this block. */ DIError DiskFSProDOS::SlurpEntries(A2File* pParent, const DirHeader* pHeader, const uint8_t* blkBuf, bool skipFirst, int* pCount, const char* basePath, uint16_t thisBlock, int depth) { DIError dierr = kDIErrNone; int entriesThisBlock = pHeader->entriesPerBlock; const uint8_t* entryBuf; A2FileProDOS* pFile; int idx = 0; entryBuf = &blkBuf[0x04]; if (skipFirst) { entriesThisBlock--; entryBuf += pHeader->entryLength; idx++; } for ( ; entriesThisBlock > 0 ; entriesThisBlock--, idx++, entryBuf += pHeader->entryLength) { if (entryBuf >= blkBuf + kBlkSize) { LOGI(" ProDOS whoops, just walked out of dirent buffer"); return kDIErrBadDirectory; } if ((entryBuf[0x00] & 0xf0) == A2FileProDOS::kStorageDeleted) { /* skip deleted entries */ continue; } pFile = new A2FileProDOS(this); if (pFile == NULL) { dierr = kDIErrMalloc; goto bail; } A2FileProDOS::DirEntry* pEntry; pEntry = &pFile->fDirEntry; A2FileProDOS::InitDirEntry(pEntry, entryBuf); pFile->SetParent(pParent); pFile->fParentDirBlock = thisBlock; pFile->fParentDirIdx = idx; pFile->SetPathName(basePath, pEntry->fileName); if (pEntry->keyPointer <= kVolHeaderBlock) { LOGI("ProDOS invalid key pointer %d on '%s'", pEntry->keyPointer, pFile->GetPathName()); pFile->SetQuality(A2File::kQualityDamaged); } else if (pEntry->storageType == A2FileProDOS::kStorageExtended) { dierr = ReadExtendedInfo(pFile); if (dierr != kDIErrNone) { pFile->SetQuality(A2File::kQualityDamaged); dierr = kDIErrNone; } } //pFile->Dump(); AddFileToList(pFile); (*pCount)++; if (!fpImg->UpdateScanProgress(NULL)) { LOGI(" ProDOS cancelled by user"); dierr = kDIErrCancelled; goto bail; } if (pEntry->storageType == A2FileProDOS::kStorageDirectory) { // don't need to check for kStorageVolumeDirHeader here dierr = RecursiveDirAdd(pFile, pEntry->keyPointer, pFile->GetPathName(), depth+1); if (dierr != kDIErrNone) { if (dierr == kDIErrCancelled) goto bail; /* mark subdir as damaged and keep going */ pFile->SetQuality(A2File::kQualityDamaged); dierr = kDIErrNone; } } } bail: return dierr; } /* * Pull the directory header out of the first block of a directory. */ DIError DiskFSProDOS::GetDirHeader(const uint8_t* blkBuf, DirHeader* pHeader) { int nameLen; pHeader->storageType = (blkBuf[0x04] & 0xf0) >> 4; if (pHeader->storageType != A2FileProDOS::kStorageSubdirHeader && pHeader->storageType != A2FileProDOS::kStorageVolumeDirHeader) { LOGI(" ProDOS WARNING: subdir header has wrong storage type (%d)", pHeader->storageType); /* keep going... might be bad idea */ } nameLen = blkBuf[0x04] & 0x0f; memcpy(pHeader->dirName, &blkBuf[0x05], nameLen); pHeader->dirName[nameLen] = '\0'; pHeader->createWhen = GetLongLE(&blkBuf[0x1c]); pHeader->version = blkBuf[0x20]; pHeader->minVersion = blkBuf[0x21]; pHeader->access = blkBuf[0x22]; pHeader->entryLength = blkBuf[0x23]; pHeader->entriesPerBlock = blkBuf[0x24]; pHeader->fileCount = GetShortLE(&blkBuf[0x25]); pHeader->parentPointer = GetShortLE(&blkBuf[0x27]); pHeader->parentEntry = blkBuf[0x29]; pHeader->parentEntryLength = blkBuf[0x2a]; if (pHeader->entryLength * pHeader->entriesPerBlock > kBlkSize || pHeader->entryLength * pHeader->entriesPerBlock == 0) { LOGI(" ProDOS invalid subdir header: entryLen=%d, entriesPerBlock=%d", pHeader->entryLength, pHeader->entriesPerBlock); return kDIErrBadDirectory; } return kDIErrNone; } /* * Read the information from the key block of an extended file. * * There's some "HFS Finder information" stuffed into the key block * right after the data fork info, but I'm planning to ignore that. */ DIError DiskFSProDOS::ReadExtendedInfo(A2FileProDOS* pFile) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, blkBuf); if (dierr != kDIErrNone) { LOGI(" ProDOS ReadExtendedInfo: unable to read key block %d", pFile->fDirEntry.keyPointer); goto bail; } pFile->fExtData.storageType = blkBuf[0x0000] & 0x0f; pFile->fExtData.keyBlock = GetShortLE(&blkBuf[0x0001]); pFile->fExtData.blocksUsed = GetShortLE(&blkBuf[0x0003]); pFile->fExtData.eof = GetLongLE(&blkBuf[0x0005]); pFile->fExtData.eof &= 0x00ffffff; pFile->fExtRsrc.storageType = blkBuf[0x0100] & 0x0f; pFile->fExtRsrc.keyBlock = GetShortLE(&blkBuf[0x0101]); pFile->fExtRsrc.blocksUsed = GetShortLE(&blkBuf[0x0103]); pFile->fExtRsrc.eof = GetLongLE(&blkBuf[0x0105]); pFile->fExtRsrc.eof &= 0x00ffffff; if (pFile->fExtData.keyBlock <= kVolHeaderBlock || pFile->fExtRsrc.keyBlock <= kVolHeaderBlock) { LOGI(" ProDOS ReadExtendedInfo: found bad extended key blocks %d/%d", pFile->fExtData.keyBlock, pFile->fExtRsrc.keyBlock); return kDIErrBadFile; } bail: return dierr; } /* * Scan all of the files on the disk, reading their block usage into the * volume usage map. This is important for detecting damage, and makes * later accesses easier. * * As a side-effect, we set the "sparse" length for the file. */ DIError DiskFSProDOS::ScanFileUsage(void) { DIError dierr = kDIErrNone; A2FileProDOS* pFile; long blockCount, indexCount, sparseCount; uint16_t* blockList = NULL; uint16_t* indexList = NULL; pFile = (A2FileProDOS*) GetNextFile(NULL); while (pFile != NULL) { if (!fpImg->UpdateScanProgress(NULL)) { LOGI(" ProDOS cancelled by user"); dierr = kDIErrCancelled; goto bail; } //pFile->Dump(); if (pFile->GetQuality() == A2File::kQualityDamaged) goto skip; if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) { /* resource fork */ if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) { /* not expecting to find a directory here, but it happens */ dierr = kDIErrBadFile; } else { dierr = pFile->LoadBlockList(pFile->fExtRsrc.storageType, pFile->fExtRsrc.keyBlock, pFile->fExtRsrc.eof, &blockCount, &blockList, &indexCount, &indexList); } if (dierr != kDIErrNone) { LOGI(" ProDOS skipping scan rsrc '%s'", pFile->fDirEntry.fileName); pFile->SetQuality(A2File::kQualityDamaged); goto skip; } ScanBlockList(blockCount, blockList, indexCount, indexList, &sparseCount); pFile->fSparseRsrcEof = (di_off_t) pFile->fExtRsrc.eof - sparseCount * kBlkSize; //LOGI(" SparseCount %d rsrcEof %d '%s'", // sparseCount, pFile->fSparseRsrcEof, pFile->fDirEntry.fileName); delete[] blockList; blockList = NULL; delete[] indexList; indexList = NULL; /* data fork */ if (!A2FileProDOS::IsRegularFile(pFile->fExtRsrc.storageType)) { dierr = kDIErrBadFile; } else { dierr = pFile->LoadBlockList(pFile->fExtData.storageType, pFile->fExtData.keyBlock, pFile->fExtData.eof, &blockCount, &blockList, &indexCount, &indexList); } if (dierr != kDIErrNone) { LOGI(" ProDOS skipping scan data '%s'", pFile->fDirEntry.fileName); pFile->SetQuality(A2File::kQualityDamaged); goto skip; } ScanBlockList(blockCount, blockList, indexCount, indexList, &sparseCount); pFile->fSparseDataEof = (di_off_t) pFile->fExtData.eof - sparseCount * kBlkSize; //LOGI(" SparseCount %ld dataEof %ld -> %lld '%s'", // sparseCount, pFile->fExtData.eof, pFile->fSparseDataEof, // pFile->fDirEntry.fileName); delete[] blockList; blockList = NULL; delete[] indexList; indexList = NULL; /* mark the extended key block as in-use */ SetBlockUsage(pFile->fDirEntry.keyPointer, VolumeUsage::kChunkPurposeFileStruct); } else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory || pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader) { /* we already got these during the recursive descent */ /* (could do them here if we used "fake" directory entry for volume dir to lead off the recursion) */ goto skip; } else if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageSeedling || pFile->fDirEntry.storageType == A2FileProDOS::kStorageSapling || pFile->fDirEntry.storageType == A2FileProDOS::kStorageTree) { /* standard file */ dierr = pFile->LoadBlockList(pFile->fDirEntry.storageType, pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof, &blockCount, &blockList, &indexCount, &indexList); if (dierr != kDIErrNone) { LOGI(" ProDOS skipping scan '%s'", pFile->fDirEntry.fileName); pFile->SetQuality(A2File::kQualityDamaged); goto skip; } ScanBlockList(blockCount, blockList, indexCount, indexList, &sparseCount); pFile->fSparseDataEof = (di_off_t) pFile->fDirEntry.eof - sparseCount * kBlkSize; //LOGI(" +++ sparseCount=%ld blockCount=%ld sparseDataEof=%lld '%s'", // sparseCount, blockCount, pFile->fSparseDataEof, // pFile->fDirEntry.fileName); delete[] blockList; blockList = NULL; delete[] indexList; indexList = NULL; } else { LOGI(" ProDOS found weird storage type %d on '%s', ignoring", pFile->fDirEntry.storageType, pFile->fDirEntry.fileName); pFile->SetQuality(A2File::kQualityDamaged); } /* * A completely empty file written as zero blocks (as opposed to simply * having its EOF extended, e.g. "sparse seedlings") will have zero data * blocks but possibly an EOF that doesn't land on 512 bytes. This can * result in a slightly negative "sparse length", which we trim to zero * here. */ //if (stricmp(pFile->fDirEntry.fileName, "EMPTY.SPARSE.R") == 0) // LOGI("wahoo"); if (pFile->fSparseDataEof < 0) pFile->fSparseDataEof = 0; if (pFile->fSparseRsrcEof < 0) pFile->fSparseRsrcEof = 0; skip: pFile = (A2FileProDOS*) GetNextFile(pFile); } dierr = kDIErrNone; bail: return dierr; } /* * Scan a block list into the volume usage map. */ void DiskFSProDOS::ScanBlockList(long blockCount, uint16_t* blockList, long indexCount, uint16_t* indexList, long* pSparseCount) { assert(blockList != NULL); assert(indexCount == 0 || indexList != NULL); assert(pSparseCount != NULL); *pSparseCount = 0; int i; for (i = 0; i < blockCount; i++) { if (blockList[i] != 0) { SetBlockUsage(blockList[i], VolumeUsage::kChunkPurposeUserData); } else { (*pSparseCount)++; // sparse data block } } for (i = 0; i < indexCount; i++) { if (indexList[i] != 0) { SetBlockUsage(indexList[i], VolumeUsage::kChunkPurposeFileStruct); } // else sparse index block } } /* * ProDOS disks may contain other filesystems. The typical DOS-in-ProDOS * strategy involves marking a bunch of blocks at the end of the disc as * "in use" without creating a file to go along with them. * * We look for certain types of embedded volume by looking for disk * usage patterns and then testing those with the standard disk testing * facilities. */ DIError DiskFSProDOS::ScanForSubVolumes(void) { DIError dierr = kDIErrNone; VolumeUsage::ChunkState cstate; int firstBlock, matchCount; int block; /* this is guaranteed by constraint in volume header read */ assert(fTotalBlocks <= fpImg->GetNumBlocks()); if (fTotalBlocks != 1600) { LOGI(" ProDOS ScanForSub: not 800K disk (%ld)", fpImg->GetNumBlocks()); return kDIErrNone; // only scan 800K disks } matchCount = 0; for (block = fTotalBlocks-1; block >= 0; block--) { if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { assert(false); return kDIErrGeneric; } if (!cstate.isMarkedUsed || cstate.isUsed) break; matchCount++; } firstBlock = block+1; LOGI("MATCH COUNT %d", matchCount); if (matchCount < 35*8) // 280 blocks on 35-track floppy return kDIErrNone; //if (matchCount % 8 != 0) { // must have 4K tracks // LOGI(" ProDOS ScanForSub: matchCount %d odd number", // matchCount); // return kDIErrNone; //} /* * Try #1: this is a single DOS 3.3 volume (200K or less). */ if ((matchCount % 8) == 0 && matchCount <= (50*8)) { // max 50 tracks DiskFS* pNewFS = NULL; DiskImg* pNewImg = NULL; LOGI(" Sub #1: looking for single DOS volume"); dierr = FindSubVolume(firstBlock, matchCount, &pNewImg, &pNewFS); if (dierr == kDIErrNone) { AddSubVolumeToList(pNewImg, pNewFS); MarkSubVolumeBlocks(firstBlock, matchCount); return kDIErrNone; } } /* * Try #2: there are multiple 140K DOS 3.3 volumes here. * * We may want to override their volume numbers, but it looks like * DOS Master disks have distinct volume numbers anyway. */ const int kBlkCount140 = 140*2; if ((matchCount % (kBlkCount140)) == 0) { int i, count; bool found = false; count = matchCount / kBlkCount140; LOGI(" Sub #2: looking for %d 140K volumes", matchCount / kBlkCount140); for (i = 0; i < count; i++) { DiskFS* pNewFS = NULL; DiskImg* pNewImg = NULL; LOGI(" Sub #2: looking for DOS volume at (%d)", firstBlock + i * kBlkCount140); dierr = FindSubVolume(firstBlock + i * kBlkCount140, kBlkCount140, &pNewImg, &pNewFS); if (dierr == kDIErrNone) { AddSubVolumeToList(pNewImg, pNewFS); MarkSubVolumeBlocks(firstBlock + i * kBlkCount140, kBlkCount140); found = true; } } if (found) return kDIErrNone; } /* * Try #3: there are five 160K DOS 3.3 volumes here (which works out * to exactly 800K). The first DOS volume loses early tracks as * needed to accommodate the ProDOS directory and up to 28K of * boot files. * * Because the first 160K volume starts at the front of the disk, * we need to restrict this to non-ProDOS sub-volumes, or we'll see * a "ghost" volume in the first position. This stuff is going to * fail if we test for ProDOS before we check for DOS 3.3. */ const int kBlkCount160 = 160*2; if (matchCount == 1537 || matchCount == 1593) { int i, count; bool found = false; count = 1600 / kBlkCount160; LOGI(" Sub #3: looking for %d 160K volumes", matchCount / kBlkCount160); for (i = 0; i < count; i++) { DiskFS* pNewFS = NULL; DiskImg* pNewImg = NULL; LOGI(" Sub #3: looking for DOS volume at (%d)", i * kBlkCount160); dierr = FindSubVolume(i * kBlkCount160, kBlkCount160, &pNewImg, &pNewFS); if (dierr == kDIErrNone) { if (pNewImg->GetFSFormat() == DiskImg::kFormatDOS33) { AddSubVolumeToList(pNewImg, pNewFS); if (i == 0) MarkSubVolumeBlocks(firstBlock, kBlkCount160 - firstBlock); else MarkSubVolumeBlocks(i * kBlkCount160, kBlkCount160); } else { delete pNewFS; delete pNewImg; pNewFS = NULL; pNewImg = NULL; } } } if (found) return kDIErrNone; } return kDIErrNone; } /* * Look for a sub-volume at the specified location. * * On success, "*ppDiskImg" and "*ppDiskFS" are newly-allocated objects * of the appropriate kind. */ DIError DiskFSProDOS::FindSubVolume(long blockStart, long blockCount, DiskImg** ppDiskImg, DiskFS** ppDiskFS) { DIError dierr = kDIErrNone; DiskFS* pNewFS = NULL; DiskImg* pNewImg = NULL; pNewImg = new DiskImg; if (pNewImg == NULL) { dierr = kDIErrMalloc; goto bail; } dierr = pNewImg->OpenImage(fpImg, blockStart, blockCount); if (dierr != kDIErrNone) { LOGI(" Sub: OpenImage(%ld,%ld) failed (err=%d)", blockStart, blockCount, dierr); goto bail; } dierr = pNewImg->AnalyzeImage(); if (dierr != kDIErrNone) { LOGI(" Sub: analysis failed (err=%d)", dierr); goto bail; } if (pNewImg->GetFSFormat() == DiskImg::kFormatUnknown || pNewImg->GetSectorOrder() == DiskImg::kSectorOrderUnknown) { LOGI(" Sub: unable to identify filesystem"); dierr = kDIErrFilesystemNotFound; goto bail; } /* open a DiskFS for the sub-image */ LOGI(" Sub DiskImg succeeded, opening DiskFS"); pNewFS = pNewImg->OpenAppropriateDiskFS(); if (pNewFS == NULL) { LOGI(" Sub: OpenAppropriateDiskFS failed"); dierr = kDIErrUnsupportedFSFmt; goto bail; } /* load the files from the sub-image */ dierr = pNewFS->Initialize(pNewImg, kInitFull); if (dierr != kDIErrNone) { LOGE(" Sub: error %d reading list of files from disk", dierr); goto bail; } bail: if (dierr != kDIErrNone) { delete pNewFS; delete pNewImg; } else { assert(pNewImg != NULL && pNewFS != NULL); *ppDiskImg = pNewImg; *ppDiskFS = pNewFS; } return dierr; } /* * Mark the blocks used by a sub-volume as in-use. */ void DiskFSProDOS::MarkSubVolumeBlocks(long block, long count) { VolumeUsage::ChunkState cstate; while (count--) { if (fVolumeUsage.GetChunkState(block, &cstate) != kDIErrNone) { assert(false); return; } assert(cstate.isMarkedUsed && !cstate.isUsed); cstate.isUsed = true; cstate.purpose = VolumeUsage::kChunkPurposeEmbedded; if (fVolumeUsage.SetChunkState(block, &cstate) != kDIErrNone) { assert(false); return; } block++; } } /* * Put a ProDOS filesystem image on the specified DiskImg. */ DIError DiskFSProDOS::Format(DiskImg* pDiskImg, const char* volName) { DIError dierr = kDIErrNone; const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0); uint8_t blkBuf[kBlkSize]; long formatBlocks; if (!IsValidVolumeName(volName)) return kDIErrInvalidArg; /* set fpImg so calls that rely on it will work; we un-set it later */ assert(fpImg == NULL); SetDiskImg(pDiskImg); LOGI(" ProDOS formatting disk image"); /* write ProDOS blocks */ dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(), DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder()); if (dierr != kDIErrNone) goto bail; formatBlocks = pDiskImg->GetNumBlocks(); if (formatBlocks > 65536) { LOGI(" ProDOS: rejecting format req blocks=%ld", formatBlocks); assert(false); return kDIErrInvalidArg; } if (formatBlocks == 65536) { LOGI(" ProDOS: trimming FS size from 65536 to 65535"); formatBlocks = 65535; } /* * We should now zero out the disk blocks, but on a 32MB volume that can * take a little while. The blocks are zeroed for us when a disk is * created, so this is really only needed if we're re-formatting an * existing disk. CiderPress currently doesn't do that, so we're going * to skip it here. */ // dierr = fpImg->ZeroImage(); LOGI(" ProDOS (not zeroing blocks)"); /* * Start by writing blocks 0 and 1 (the boot blocks). This is done from * a standard boot block image that happens to be essentially the same * for all types of disks. (Apparently these blocks are only used when * booting 5.25" disks?) */ dierr = WriteBootBlocks(); if (dierr != kDIErrNone) goto bail; /* * Write the four-block disk volume entry. Start by writing the three * empty ones (which only have the prev/next pointers), and finish by * writing the first block, which has the volume directory header. */ int i; memset(blkBuf, 0, sizeof(blkBuf)); for (i = kVolHeaderBlock+1; i < kVolHeaderBlock+kFormatVolDirNumBlocks; i++) { PutShortLE(&blkBuf[0x00], i-1); if (i == kVolHeaderBlock+kFormatVolDirNumBlocks-1) PutShortLE(&blkBuf[0x02], 0); else PutShortLE(&blkBuf[0x02], i+1); dierr = fpImg->WriteBlock(i, blkBuf); if (dierr != kDIErrNone) { LOGI(" Format: block %d write failed (err=%d)", i, dierr); goto bail; } } char upperName[A2FileProDOS::kMaxFileName+1]; uint16_t lcFlags; time_t now; now = time(NULL); /* * Compute the lower-case flags, if desired. The test for "allowLowerCase" * is probably bogus, because in most cases we just got created by the * DiskImg and the app hasn't had time to set the "allow lower" flag. * So it defaults to "enabled", which means the app needs to manually * change the volume name to lower case. */ UpperCaseName(upperName, volName); lcFlags = 0; if (allowLowerCase) lcFlags = GenerateLowerCaseBits(upperName, volName, false); PutShortLE(&blkBuf[0x00], 0); PutShortLE(&blkBuf[0x02], kVolHeaderBlock+1); blkBuf[0x04] = (uint8_t)(strlen(upperName) | (A2FileProDOS::kStorageVolumeDirHeader << 4)); strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName); PutLongLE(&blkBuf[0x16], A2FileProDOS::ConvertProDate(now)); PutShortLE(&blkBuf[0x1a], lcFlags); PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(now)); blkBuf[0x20] = 0; // GS/OS uses 5? /* min_version is zero */ blkBuf[0x22] = 0xe3; // access (format/rename/backup/write/read) blkBuf[0x23] = 0x27; // entry_length: always $27 blkBuf[0x24] = 0x0d; // entries_per_block: always $0d /* file_count is zero - does not include volume dir */ PutShortLE(&blkBuf[0x27], kVolHeaderBlock + kFormatVolDirNumBlocks); // bit_map_pointer PutShortLE(&blkBuf[0x29], (uint16_t) formatBlocks); // total_blocks dierr = fpImg->WriteBlock(kVolHeaderBlock, blkBuf); if (dierr != kDIErrNone) { LOGI(" Format: block %d write failed (err=%d)", kVolHeaderBlock, dierr); goto bail; } /* check our work, and set some object fields, by reading what we wrote */ dierr = LoadVolHeader(); if (dierr != kDIErrNone) { LOGI(" GLITCH: couldn't read header we just wrote (err=%d)", dierr); goto bail; } /* * Generate the initial block usage map. The only entries in use are * right at the start of the disk. */ CreateEmptyBlockMap(); /* don't do this -- assume they're going to call Initialize() later */ //ScanVolBitmap(); bail: SetDiskImg(NULL); // shouldn't really be set by us return dierr; } /* * The standard boot block found on ProDOS disks. The same thing appears * to be written to both 5.25" and 3.5" disks, with some modifications * made for HD images. * * This is block 0; block 1 is either zeroed out or filled with a repeating * pattern. */ const uint8_t gFloppyBlock0[512] = { 0x01, 0x38, 0xb0, 0x03, 0x4c, 0x32, 0xa1, 0x86, 0x43, 0xc9, 0x03, 0x08, 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, 0xa0, 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, 0xa9, 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, 0x48, 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x63, 0xb1, 0x48, 0x99, 0x94, 0x09, 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x1d, 0x09, 0xbd, 0x24, 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x2b, 0x09, 0x9d, 0x7f, 0x0a, 0xca, 0x10, 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, 0xb0, 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, 0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, 0x85, 0x4b, 0x20, 0x12, 0x09, 0xb0, 0x68, 0xe6, 0x61, 0xe6, 0x61, 0xe6, 0x46, 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, 0x0c, 0xd0, 0x6d, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, 0x0c, 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, 0x0a, 0xf0, 0x55, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x02, 0x09, 0x29, 0x0f, 0xa8, 0xb1, 0x4a, 0xd9, 0x02, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0x29, 0xf0, 0xc9, 0x20, 0xd0, 0x3b, 0xa0, 0x10, 0xb1, 0x4a, 0xc9, 0xff, 0xd0, 0x33, 0xc8, 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, 0x85, 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, 0x12, 0x09, 0xb0, 0x17, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, 0xb1, 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0xe7, 0x4c, 0x00, 0x20, 0x4c, 0x3f, 0x09, 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0xa5, 0x60, 0x85, 0x44, 0xa5, 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, 0x3f, 0x45, 0x47, 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, 0x2b, 0x18, 0x60, 0x4c, 0xbc, 0x09, 0xa9, 0x9f, 0x48, 0xa9, 0xff, 0x48, 0xa9, 0x01, 0xa2, 0x00, 0x4c, 0x79, 0xf4, 0x20, 0x58, 0xfc, 0xa0, 0x1c, 0xb9, 0x50, 0x09, 0x99, 0xae, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x4d, 0x09, 0xaa, 0xaa, 0xaa, 0xa0, 0xd5, 0xce, 0xc1, 0xc2, 0xcc, 0xc5, 0xa0, 0xd4, 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, 0xd2, 0xcf, 0xc4, 0xcf, 0xd3, 0xa0, 0xaa, 0xaa, 0xaa, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b, 0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9, 0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04, 0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5, 0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85, 0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6, 0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60, 0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85, 0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02, 0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09, 0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0, 0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; const uint8_t gHDBlock0[] = { 0x01, 0x38, 0xb0, 0x03, 0x4c, 0x1c, 0x09, 0x78, 0x86, 0x43, 0xc9, 0x03, 0x08, 0x8a, 0x29, 0x70, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x49, 0xa0, 0xff, 0x84, 0x48, 0x28, 0xc8, 0xb1, 0x48, 0xd0, 0x3a, 0xb0, 0x0e, 0xa9, 0x03, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0xa5, 0x49, 0x48, 0xa9, 0x5b, 0x48, 0x60, 0x85, 0x40, 0x85, 0x48, 0xa0, 0x5e, 0xb1, 0x48, 0x99, 0x94, 0x09, 0xc8, 0xc0, 0xeb, 0xd0, 0xf6, 0xa2, 0x06, 0xbc, 0x32, 0x09, 0xbd, 0x39, 0x09, 0x99, 0xf2, 0x09, 0xbd, 0x40, 0x09, 0x9d, 0x7f, 0x0a, 0xca, 0x10, 0xee, 0xa9, 0x09, 0x85, 0x49, 0xa9, 0x86, 0xa0, 0x00, 0xc9, 0xf9, 0xb0, 0x2f, 0x85, 0x48, 0x84, 0x60, 0x84, 0x4a, 0x84, 0x4c, 0x84, 0x4e, 0x84, 0x47, 0xc8, 0x84, 0x42, 0xc8, 0x84, 0x46, 0xa9, 0x0c, 0x85, 0x61, 0x85, 0x4b, 0x20, 0x27, 0x09, 0xb0, 0x66, 0xe6, 0x61, 0xe6, 0x61, 0xe6, 0x46, 0xa5, 0x46, 0xc9, 0x06, 0x90, 0xef, 0xad, 0x00, 0x0c, 0x0d, 0x01, 0x0c, 0xd0, 0x52, 0xa9, 0x04, 0xd0, 0x02, 0xa5, 0x4a, 0x18, 0x6d, 0x23, 0x0c, 0xa8, 0x90, 0x0d, 0xe6, 0x4b, 0xa5, 0x4b, 0x4a, 0xb0, 0x06, 0xc9, 0x0a, 0xf0, 0x71, 0xa0, 0x04, 0x84, 0x4a, 0xad, 0x20, 0x09, 0x29, 0x0f, 0xa8, 0xb1, 0x4a, 0xd9, 0x20, 0x09, 0xd0, 0xdb, 0x88, 0x10, 0xf6, 0xa0, 0x16, 0xb1, 0x4a, 0x4a, 0x6d, 0x1f, 0x09, 0x8d, 0x1f, 0x09, 0xa0, 0x11, 0xb1, 0x4a, 0x85, 0x46, 0xc8, 0xb1, 0x4a, 0x85, 0x47, 0xa9, 0x00, 0x85, 0x4a, 0xa0, 0x1e, 0x84, 0x4b, 0x84, 0x61, 0xc8, 0x84, 0x4d, 0x20, 0x27, 0x09, 0xb0, 0x35, 0xe6, 0x61, 0xe6, 0x61, 0xa4, 0x4e, 0xe6, 0x4e, 0xb1, 0x4a, 0x85, 0x46, 0xb1, 0x4c, 0x85, 0x47, 0x11, 0x4a, 0xd0, 0x18, 0xa2, 0x01, 0xa9, 0x00, 0xa8, 0x91, 0x60, 0xc8, 0xd0, 0xfb, 0xe6, 0x61, 0xea, 0xea, 0xca, 0x10, 0xf4, 0xce, 0x1f, 0x09, 0xf0, 0x07, 0xd0, 0xd8, 0xce, 0x1f, 0x09, 0xd0, 0xca, 0x58, 0x4c, 0x00, 0x20, 0x4c, 0x47, 0x09, 0x02, 0x26, 0x50, 0x52, 0x4f, 0x44, 0x4f, 0x53, 0xa5, 0x60, 0x85, 0x44, 0xa5, 0x61, 0x85, 0x45, 0x6c, 0x48, 0x00, 0x08, 0x1e, 0x24, 0x3f, 0x45, 0x47, 0x76, 0xf4, 0xd7, 0xd1, 0xb6, 0x4b, 0xb4, 0xac, 0xa6, 0x2b, 0x18, 0x60, 0x4c, 0xbc, 0x09, 0x20, 0x58, 0xfc, 0xa0, 0x14, 0xb9, 0x58, 0x09, 0x99, 0xb1, 0x05, 0x88, 0x10, 0xf7, 0x4c, 0x55, 0x09, 0xd5, 0xce, 0xc1, 0xc2, 0xcc, 0xc5, 0xa0, 0xd4, 0xcf, 0xa0, 0xcc, 0xcf, 0xc1, 0xc4, 0xa0, 0xd0, 0xd2, 0xcf, 0xc4, 0xcf, 0xd3, 0xa5, 0x53, 0x29, 0x03, 0x2a, 0x05, 0x2b, 0xaa, 0xbd, 0x80, 0xc0, 0xa9, 0x2c, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe9, 0x01, 0xd0, 0xf7, 0xa6, 0x2b, 0x60, 0xa5, 0x46, 0x29, 0x07, 0xc9, 0x04, 0x29, 0x03, 0x08, 0x0a, 0x28, 0x2a, 0x85, 0x3d, 0xa5, 0x47, 0x4a, 0xa5, 0x46, 0x6a, 0x4a, 0x4a, 0x85, 0x41, 0x0a, 0x85, 0x51, 0xa5, 0x45, 0x85, 0x27, 0xa6, 0x2b, 0xbd, 0x89, 0xc0, 0x20, 0xbc, 0x09, 0xe6, 0x27, 0xe6, 0x3d, 0xe6, 0x3d, 0xb0, 0x03, 0x20, 0xbc, 0x09, 0xbc, 0x88, 0xc0, 0x60, 0xa5, 0x40, 0x0a, 0x85, 0x53, 0xa9, 0x00, 0x85, 0x54, 0xa5, 0x53, 0x85, 0x50, 0x38, 0xe5, 0x51, 0xf0, 0x14, 0xb0, 0x04, 0xe6, 0x53, 0x90, 0x02, 0xc6, 0x53, 0x38, 0x20, 0x6d, 0x09, 0xa5, 0x50, 0x18, 0x20, 0x6f, 0x09, 0xd0, 0xe3, 0xa0, 0x7f, 0x84, 0x52, 0x08, 0x28, 0x38, 0xc6, 0x52, 0xf0, 0xce, 0x18, 0x08, 0x88, 0xf0, 0xf5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /* * Write the ProDOS boot blocks onto the disk image. */ DIError DiskFSProDOS::WriteBootBlocks(void) { DIError dierr; uint8_t block0[512]; uint8_t block1[512]; bool isHD; assert(fpImg->GetHasBlocks()); if (fpImg->GetNumBlocks() == 280 || fpImg->GetNumBlocks() == 1600) isHD = false; else isHD = true; if (isHD) { memcpy(block0, gHDBlock0, sizeof(block0)); // repeating 0x42 0x48 pattern int i; uint8_t* ucp; for (i = 0, ucp = block1; i < (int)sizeof(block1); i++) *ucp++ = 0x42 + 6 * (i & 0x01); } else { memcpy(block0, gFloppyBlock0, sizeof(block0)); memset(block1, 0, sizeof(block1)); } dierr = fpImg->WriteBlock(0, block0); if (dierr != kDIErrNone) { LOGI(" WriteBootBlocks: block0 write failed (err=%d)", dierr); return dierr; } dierr = fpImg->WriteBlock(1, block1); if (dierr != kDIErrNone) { LOGI(" WriteBootBlocks: block1 write failed (err=%d)", dierr); return dierr; } return kDIErrNone; } /* * Create a new, empty file. There are three different kinds of files we * need to be able to handle: * (1) Standard file. Create the directory entry and an empty "seedling" * file with one block allocated. It does not appear that "sparse" * allocation applies to seedlings. * (2) Extended file. Create the directory entry, the extended key block, * and allocate one seedling block for each fork. * (3) Subdirectory. Allocate a block for the subdir and fill in the * details in the subdir header. * * In all cases we need to add a new directory entry as well. * * By not flushing the updated block usage map and the updated directory * block(s) until we're done, we can abort our changes at any time if we * encounter a damaged sector or run out of disk space. We do need to be * careful when updating our internal copies of things like file storage * types and lengths, updating them only after everything else has * succeeded. * * NOTE: if we detect an empty directory holder, "*ppNewFile" does NOT * end up pointing at a file. * * NOTE: kParm_CreateUnique does *not* apply to creating subdirectories. */ DIError DiskFSProDOS::CreateFile(const CreateParms* pParms, A2File** ppNewFile) { DIError dierr = kDIErrNone; char* normalizedPath = NULL; char* basePath = NULL; char* fileName = NULL; A2FileProDOS* pSubdir = NULL; A2FileDescr* pOpenSubdir = NULL; A2FileProDOS* pNewFile = NULL; uint8_t* subdirBuf = NULL; const bool allowLowerCase = (GetParameter(kParmProDOS_AllowLowerCase) != 0); const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); char upperName[A2FileProDOS::kMaxFileName+1]; char lowerName[A2FileProDOS::kMaxFileName+1]; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; assert(pParms != NULL); assert(pParms->pathName != NULL); assert(pParms->storageType == A2FileProDOS::kStorageSeedling || pParms->storageType == A2FileProDOS::kStorageExtended || pParms->storageType == A2FileProDOS::kStorageDirectory); // kStorageVolumeDirHeader not allowed -- that's created by Format LOGI(" ProDOS ---v--- CreateFile '%s'", pParms->pathName); *ppNewFile = NULL; /* * Normalize the pathname so that all components are ProDOS-safe * and separated by ':'. */ assert(pParms->pathName != NULL); dierr = DoNormalizePath(pParms->pathName, pParms->fssep, &normalizedPath); if (dierr != kDIErrNone) goto bail; assert(normalizedPath != NULL); /* * Split the base path and filename apart. */ char* cp; cp = strrchr(normalizedPath, A2FileProDOS::kFssep); if (cp == NULL) { assert(basePath == NULL); fileName = normalizedPath; } else { fileName = new char[strlen(cp+1) +1]; strcpy(fileName, cp+1); *cp = '\0'; basePath = normalizedPath; } normalizedPath = NULL; // either fileName or basePath points here now assert(fileName != NULL); //LOGI(" ProDOS normalized to '%s':'%s'", // basePath == NULL ? "" : basePath, fileName); /* * Open the base path. If it doesn't exist, create it recursively. */ if (basePath != NULL) { LOGI(" ProDOS Creating '%s' in '%s'", fileName, basePath); /* open the named subdir, creating it if it doesn't exist */ pSubdir = (A2FileProDOS*)GetFileByName(basePath); if (pSubdir == NULL) { LOGI(" ProDOS Creating subdir '%s'", basePath); A2File* pNewSub; CreateParms newDirParms; newDirParms.pathName = basePath; newDirParms.fssep = A2FileProDOS::kFssep; newDirParms.storageType = A2FileProDOS::kStorageDirectory; newDirParms.fileType = kTypeDIR; // 0x0f newDirParms.auxType = 0; newDirParms.access = 0xe3; // unlocked, backup bit set newDirParms.createWhen = newDirParms.modWhen = time(NULL); dierr = this->CreateFile(&newDirParms, &pNewSub); if (dierr != kDIErrNone) goto bail; assert(pNewSub != NULL); pSubdir = (A2FileProDOS*) pNewSub; } /* * And now the annoying part. We need to reconstruct basePath out * of the filenames actually present, rather than relying on the * argument passed in. That's because some directories might have * lower-case flags and some might not, and we do case-insensitive * comparisons. It's not crucial for our inner workings, but the * linear file list in the DiskFS should have accurate strings. * (It'll work just fine, but the display might show the wrong values * for parent directories until they reload the disk.) * * On the bright side, we know exactly how long the string needs * to be, so we can just stomp on it in place. Assuming, of course, * that the filename created matches up with what the filename * normalizer came up with, which we can guarantee since (a) everybody * uses the same normalizer and (b) the "uniqueify" stuff doesn't * kick in for subdirs because we wouldn't be creating a new subdir * if it didn't already exist. * * This is essentially the same as RegeneratePathName(), but that's * meant for a situation where the filename already exists. */ A2FileProDOS* pBaseDir = pSubdir; int basePathLen = strlen(basePath); while (!pBaseDir->IsVolumeDirectory()) { const char* fixedName = pBaseDir->GetFileName(); int fixedLen = strlen(fixedName); if (fixedLen > basePathLen) { assert(false); break; } assert(basePathLen == fixedLen || *(basePath + (basePathLen-fixedLen-1)) == kDIFssep); memcpy(basePath + (basePathLen-fixedLen), fixedName, fixedLen); basePathLen -= fixedLen+1; pBaseDir = (A2FileProDOS*) pBaseDir->GetParent(); assert(pBaseDir != NULL); } // check the math if (pSubdir->IsVolumeDirectory()) assert(basePathLen == 0); else assert(basePathLen == -1); } else { /* open the volume directory */ LOGI(" ProDOS Creating '%s' in volume dir", fileName); /* volume dir must be first in the list */ pSubdir = (A2FileProDOS*) GetNextFile(NULL); assert(pSubdir != NULL); assert(pSubdir->IsVolumeDirectory()); } if (pSubdir == NULL) { LOGI(" ProDOS Unable to open subdir '%s'", basePath); dierr = kDIErrFileNotFound; goto bail; } /* * Load the block usage map into memory. All changes, to the end of this * function, are made to the in-memory copy and can be "undone" by simply * throwing the temporary map away. */ dierr = LoadVolBitmap(); if (dierr != kDIErrNone) return dierr; /* * Load the subdir or volume dir into memory, and alloc a new directory * entry. */ dierr = pSubdir->Open(&pOpenSubdir, false); if (dierr != kDIErrNone) goto bail; uint8_t* dirEntryPtr; long dirLen; uint16_t dirBlock, dirKeyBlock; int dirEntrySlot; dierr = AllocDirEntry(pOpenSubdir, &subdirBuf, &dirLen, &dirEntryPtr, &dirKeyBlock, &dirEntrySlot, &dirBlock); if (dierr != kDIErrNone) goto bail; assert(subdirBuf != NULL); assert(dirLen > 0); assert(dirKeyBlock > 0); assert(dirEntrySlot >= 0); assert(dirBlock > 0); /* * Create a copy of the filename with everything in upper case and spaces * changed to periods. */ UpperCaseName(upperName, fileName); /* * Make the name unique within the current directory. This requires * appending digits until the name doesn't match any others. * * The filename buffer ("upperName") must be able to hold kMaxFileName+1 * chars. It will be modified in place. */ if (createUnique && pParms->storageType != A2FileProDOS::kStorageDirectory) { MakeFileNameUnique(subdirBuf, dirLen, upperName); } else { /* check to see if it already exists */ if (NameExistsInDir(subdirBuf, dirLen, upperName)) { if (pParms->storageType == A2FileProDOS::kStorageDirectory) dierr = kDIErrDirectoryExists; else dierr = kDIErrFileExists; goto bail; } } /* * Allocate file storage and initialize: * - For directory, a single block with the directory header. * - For seedling, an empty block. * - For extended, an extended key block entry and two empty blocks. */ long keyBlock; int blocksUsed; int newEOF; keyBlock = -1; blocksUsed = newEOF = -1; dierr = AllocInitialFileStorage(pParms, upperName, dirBlock, dirEntrySlot, &keyBlock, &blocksUsed, &newEOF); if (dierr != kDIErrNone) goto bail; assert(blocksUsed > 0); assert(keyBlock > 0); assert(newEOF >= 0); /* * Fill out the newly-created directory entry pointed to by "dirEntryPtr". * * ProDOS filenames are always stored in upper case. ProDOS 8 v1.8 and * later allow lower-case names with '.' converting to ' '. We optionally * set the flags here, using the original file name to decide which parts * are lower case. (Some parts of the original may have been stomped * when the name was made unique, so we need to watch for that.) */ dirEntryPtr[0x00] = (uint8_t)((pParms->storageType << 4) | strlen(upperName)); strncpy((char*) &dirEntryPtr[0x01], upperName, A2FileProDOS::kMaxFileName); if (pParms->fileType >= 0 && pParms->fileType <= 0xff) dirEntryPtr[0x10] = (uint8_t) pParms->fileType; else dirEntryPtr[0x10] = 0; // HFS long type? PutShortLE(&dirEntryPtr[0x11], (uint16_t) keyBlock); PutShortLE(&dirEntryPtr[0x13], blocksUsed); PutShortLE(&dirEntryPtr[0x15], newEOF); dirEntryPtr[0x17] = 0; // high byte of EOF PutLongLE(&dirEntryPtr[0x18], A2FileProDOS::ConvertProDate(pParms->createWhen)); if (allowLowerCase) { uint16_t lcBits; lcBits = GenerateLowerCaseBits(upperName, fileName, false); GenerateLowerCaseName(upperName, lowerName, lcBits, false); lowerName[strlen(upperName)] = '\0'; PutShortLE(&dirEntryPtr[0x1c], lcBits); } else { strcpy(lowerName, upperName); PutShortLE(&dirEntryPtr[0x1c], 0); // version, min_version } dirEntryPtr[0x1e] = pParms->access; if (pParms->auxType >= 0 && pParms->auxType <= 0xffff) PutShortLE(&dirEntryPtr[0x1f], (uint16_t) pParms->auxType); else PutShortLE(&dirEntryPtr[0x1f], 0); PutLongLE(&dirEntryPtr[0x21], A2FileProDOS::ConvertProDate(pParms->modWhen)); PutShortLE(&dirEntryPtr[0x25], dirKeyBlock); /* * Write updated directory. If this succeeds, we can no longer undo * what we have done by simply bailing. If this fails partway through, * we might have a corrupted disk, so it's best to ensure that it's not * going to fail before we call. * * Assuming this isn't a nibble image with I/O errors, the only way we * can really fail is by running out of disk space. The block has been * pre-allocated, so this should always work. */ dierr = pOpenSubdir->Write(subdirBuf, dirLen); if (dierr != kDIErrNone) { LOGI(" ProDOS directory write failed (dirLen=%ld)", dirLen); goto bail; } /* * Flush updated block usage map. */ dierr = SaveVolBitmap(); if (dierr != kDIErrNone) goto bail; /* * Success! * * Create an A2File entry for this, and add it to the list. The calls * below will re-process some of what we just created, which is slightly * inefficient but helps guarantee that we aren't creating bogus data * structures that won't match what we see when the disk is reloaded. * * - Regen or update internal VolumeUsage map?? Throw it away or mark * it as invalid? */ pNewFile = new A2FileProDOS(this); A2FileProDOS::DirEntry* pEntry; pEntry = &pNewFile->fDirEntry; A2FileProDOS::InitDirEntry(pEntry, dirEntryPtr); pNewFile->fParentDirBlock = dirBlock; pNewFile->fParentDirIdx = (dirEntrySlot-1) % kEntriesPerBlock; pNewFile->fSparseDataEof = 0; pNewFile->fSparseRsrcEof = 0; /* * Get the properly-cased filename for the file list. We already have * a name in "lowerName", but it doesn't take AppleWorks aux type * case stuff into account. If necessary, deal with it now. */ if (A2FileProDOS::UsesAppleWorksAuxType(pNewFile->fDirEntry.fileType)) { DiskFSProDOS::GenerateLowerCaseName(pNewFile->fDirEntry.fileName, lowerName, pNewFile->fDirEntry.auxType, true); } pNewFile->SetPathName(basePath == NULL ? "" : basePath, lowerName); if (pEntry->storageType == A2FileProDOS::kStorageExtended) { dierr = ReadExtendedInfo(pNewFile); if (dierr != kDIErrNone) { LOGI(" ProDOS GLITCH: readback of extended block failed!"); delete pNewFile; goto bail; } } pNewFile->SetParent(pSubdir); //pNewFile->Dump(); /* * Because we're hierarchical, and we guarantee that the contents of * subdirectories are grouped together, we must insert the file into an * appropriate place in the list rather than just throwing it onto the * end. * * The proper location for the new file in the linear list is after the * previous file in our subdir. If we're the first item in the subdir, * we get added right after the parent. If not, we need to scan, starting * from the parent, for an entry in the file list whose key block pointer * matches that of the previous item in the list. * * We wouldn't be this far if the disk were damaged, so we don't have to * worry too much about weirdness. The directory entry allocator always * returns the first available, so we know the previous entry is valid. */ uint8_t* prevDirEntryPtr; prevDirEntryPtr = GetPrevDirEntry(subdirBuf, dirEntryPtr); if (prevDirEntryPtr == NULL) { /* previous entry is volume or subdir header */ InsertFileInList(pNewFile, pNewFile->GetParent()); LOGI("Inserted '%s' after '%s'", pNewFile->GetPathName(), pNewFile->GetParent()->GetPathName()); } else { /* dig out the key block pointer and find the matching file */ uint16_t prevKeyBlock; assert((prevDirEntryPtr[0x00] & 0xf0) != 0); // verify storage type prevKeyBlock = GetShortLE(&prevDirEntryPtr[0x11]); A2File* pPrev; pPrev = FindFileByKeyBlock(pNewFile->GetParent(), prevKeyBlock); if (pPrev == NULL) { /* should be impossible! */ assert(false); AddFileToList(pNewFile); } else { /* insert the new file in the list after the previous file */ InsertFileInList(pNewFile, pPrev); } } // LOGI("LIST NOW:"); // DumpFileList(); *ppNewFile = pNewFile; pNewFile = NULL; bail: delete pNewFile; if (pOpenSubdir != NULL) pOpenSubdir->Close(); // writes updated dir entry in parent dir FreeVolBitmap(); delete[] normalizedPath; delete[] subdirBuf; delete[] fileName; delete[] basePath; LOGI(" ProDOS ---^--- CreateFile '%s' DONE", pParms->pathName); return dierr; } /* * Run through the DiskFS file list, looking for an entry with a matching * key block. */ A2File* DiskFSProDOS::FindFileByKeyBlock(A2File* pStart, uint16_t keyBlock) { while (pStart != NULL) { A2FileProDOS* pPro = (A2FileProDOS*) pStart; if (pPro->fDirEntry.keyPointer == keyBlock) return pStart; pStart = GetNextFile(pStart); } return NULL; } /* * Allocate the initial storage (key blocks, directory header) for a new file. * * Output values are the key block for the new file, the number of blocks * used, and an EOF value. * * "upperName" is the upper-case name for the file. "dirBlock" and * "dirEntrySlot" refer to the entry in the higher-level directory for this * file, and are only needed when creating a new subdir (because the first * entry in a subdir points to its entry in the parent dir). */ DIError DiskFSProDOS::AllocInitialFileStorage(const CreateParms* pParms, const char* upperName, uint16_t dirBlock, int dirEntrySlot, long* pKeyBlock, int* pBlocksUsed, int* pNewEOF) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; long keyBlock; int blocksUsed; int newEOF; blocksUsed = -1; keyBlock = -1; newEOF = 0; memset(blkBuf, 0, sizeof(blkBuf)); if (pParms->storageType == A2FileProDOS::kStorageSeedling) { keyBlock = AllocBlock(); if (keyBlock == -1) { dierr = kDIErrDiskFull; goto bail; } blocksUsed = 1; /* write zeroed block */ dierr = fpImg->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; } else if (pParms->storageType == A2FileProDOS::kStorageExtended) { long dataBlock, rsrcBlock; dataBlock = AllocBlock(); rsrcBlock = AllocBlock(); keyBlock = AllocBlock(); if (dataBlock < 0 || rsrcBlock < 0 || keyBlock < 0) { dierr = kDIErrDiskFull; goto bail; } blocksUsed = 3; newEOF = kBlkSize; /* write zeroed block */ dierr = fpImg->WriteBlock(dataBlock, blkBuf); if (dierr != kDIErrNone) goto bail; dierr = fpImg->WriteBlock(rsrcBlock, blkBuf); if (dierr != kDIErrNone) goto bail; /* fill in extended key block details */ blkBuf[0x00] = blkBuf[0x100] = A2FileProDOS::kStorageSeedling; PutShortLE(&blkBuf[0x01], (uint16_t) dataBlock); PutShortLE(&blkBuf[0x101], (uint16_t) rsrcBlock); blkBuf[0x03] = blkBuf[0x103] = 1; // blocks used (lo byte) /* 3 bytes at 0x05 hold EOF, currently 0 */ dierr = fpImg->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; } else if (pParms->storageType == A2FileProDOS::kStorageDirectory) { keyBlock = AllocBlock(); if (keyBlock == -1) { dierr = kDIErrDiskFull; goto bail; } blocksUsed = 1; newEOF = kBlkSize; /* fill in directory header fields */ // 0x00: prev, set to zero // 0x02: next, set to zero blkBuf[0x04] = (uint8_t)((A2FileProDOS::kStorageSubdirHeader << 4) | strlen(upperName)); strncpy((char*) &blkBuf[0x05], upperName, A2FileProDOS::kMaxFileName); blkBuf[0x14] = 0x76; // 0x75 under old P8, 0x76 under GS/OS PutLongLE(&blkBuf[0x1c], A2FileProDOS::ConvertProDate(pParms->createWhen)); blkBuf[0x20] = 5; // 0 under 1.0, 3 under v1.4?, 5 under GS/OS blkBuf[0x21] = 0; blkBuf[0x22] = pParms->access; blkBuf[0x23] = kEntryLength; blkBuf[0x24] = kEntriesPerBlock; PutShortLE(&blkBuf[0x25], 0); // file count PutShortLE(&blkBuf[0x27], dirBlock); blkBuf[0x29] = (uint8_t) dirEntrySlot; blkBuf[0x2a] = kEntryLength; // the parent dir's entry length dierr = fpImg->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; } else { assert(false); dierr = kDIErrInternal; goto bail; } *pKeyBlock = keyBlock; *pBlocksUsed = blocksUsed; *pNewEOF = newEOF; bail: return dierr; } /* * Scan for damaged files and mysterious or conflicting block usage map * entries. * * Appends some entries to the DiskImg notes, so this should only be run * once per DiskFS. * * This function doesn't set anything; it's effectively "const" except * that LoadVolBitmap is inherently non-const. * * Returns "true" if disk appears to be perfect, "false" otherwise. */ bool DiskFSProDOS::CheckDiskIsGood(void) { DIError dierr; bool result = true; int i; if (fEarlyDamage) result = false; dierr = LoadVolBitmap(); if (dierr != kDIErrNone) goto bail; /* * Check the system blocks to see if any of them are marked as free. * If so, refuse to write to this disk. */ if (!GetBlockUseEntry(0) || !GetBlockUseEntry(1)) { fpImg->AddNote(DiskImg::kNoteWarning, "Block 0/1 marked as free."); result = false; } for (i = GetNumBitmapBlocks(); i > 0; i--) { if (!GetBlockUseEntry(fBitMapPointer + i -1)) { fpImg->AddNote(DiskImg::kNoteWarning, "One or more bitmap blocks are marked as free."); result = false; break; } } /* * Check for used blocks that aren't marked in-use. * * This requires that VolumeUsage be accurate. Since this function is * only run during initial startup, any later deviation between VU and * the block use map is irrelevant. */ VolumeUsage::ChunkState cstate; long blk, notMarked, extraUsed, conflicts; notMarked = extraUsed = conflicts = 0; for (blk = 0; blk < fVolumeUsage.GetNumChunks(); blk++) { dierr = fVolumeUsage.GetChunkState(blk, &cstate); if (dierr != kDIErrNone) { fpImg->AddNote(DiskImg::kNoteWarning, "Internal volume usage error on blk=%ld.", blk); result = false; goto bail; } if (cstate.isUsed && !cstate.isMarkedUsed) notMarked++; if (!cstate.isUsed && cstate.isMarkedUsed) extraUsed++; if (cstate.purpose == VolumeUsage::kChunkPurposeConflict) conflicts++; } if (extraUsed > 0) { fpImg->AddNote(DiskImg::kNoteInfo, "%ld block%s marked used but not part of any file.", extraUsed, extraUsed == 1 ? " is" : "s are"); // not a problem, really } if (notMarked > 0) { fpImg->AddNote(DiskImg::kNoteWarning, "%ld block%s used by files but not marked used.", notMarked, notMarked == 1 ? " is" : "s are"); result = false; // very bad -- any change could trash files } if (conflicts > 0) { fpImg->AddNote(DiskImg::kNoteWarning, "%ld block%s used by more than one file.", conflicts, conflicts == 1 ? " is" : "s are"); result = false; // kinda bad -- file deletion leads to trouble } /* * Check for bits set past the end of the actually-needed bits. For * some reason P8 and GS/OS both examine these bits, and GS/OS will * freak out completely and claim the disk is unrecognizeable ("would * you like to format?") if they're set. */ if (ScanForExtraEntries()) { fpImg->AddNote(DiskImg::kNoteWarning, "Blocks past the end of the disk are marked 'in use' in the" " volume bitmap."); /* don't flunk the disk just for this */ } /* * Scan for "damaged" or "suspicious" files diagnosed earlier. */ bool damaged, suspicious; ScanForDamagedFiles(&damaged, &suspicious); if (damaged) { fpImg->AddNote(DiskImg::kNoteWarning, "One or more files are damaged."); result = false; } else if (suspicious) { fpImg->AddNote(DiskImg::kNoteWarning, "One or more files look suspicious."); result = false; } bail: FreeVolBitmap(); return result; } /* * Test a string for validity as a ProDOS volume name. Syntax is the same as * ProDOS file names, but we also disallow spaces. */ /*static*/ bool DiskFSProDOS::IsValidVolumeName(const char* name) { assert((int) A2FileProDOS::kMaxFileName == (int) kMaxVolumeName); if (!IsValidFileName(name)) return false; while (*name != '\0') { if (*name++ == ' ') return false; } return true; } /* * Test a string for validity as a ProDOS file name. Names may be 1-15 * characters long, must start with a letter, and may contain letters and * digits. * * Lower case and spaces (a/k/a lower-case '.') are accepted. Trailing * spaces are not allowed. */ /*static*/ bool DiskFSProDOS::IsValidFileName(const char* name) { if (name == NULL) { assert(false); return false; } /* must be 1-15 characters long */ if (name[0] == '\0') return false; if (strlen(name) > A2FileProDOS::kMaxFileName) return false; /* must begin with letter; this also catches zero-length filenames */ if (toupper(name[0]) < 'A' || toupper(name[0]) > 'Z') return false; /* no trailing spaces */ if (name[strlen(name)-1] == ' ') return false; /* must be A-Za-z 0-9 '.' ' ' */ name++; while (*name != '\0') { if (!( (toupper(*name) >= 'A' && toupper(*name) <= 'Z') || (*name >= '0' && *name <= '9') || (*name == '.') || (*name == ' ') )) { return false; } name++; } return true; } /* * Generate lower case flags by comparing "upperName" to "lowerName". * * It's okay for "lowerName" to be longer than "upperName". The extra chars * are just ignored. Similarly, "lowerName" does not need to be * null-terminated. "lowerName" does need to point to storage with at least * as many valid bytes as "upperName", though, or we could crash. * * Returns the mask to use in a ProDOS dir. If "forAppleWorks" is set to * "true", the mask is modified for use with an AppleWorks aux type. */ /*static*/ uint16_t DiskFSProDOS::GenerateLowerCaseBits(const char* upperName, const char* lowerName, bool forAppleWorks) { uint16_t caseMask = 0x8000; uint16_t caseBit = 0x8000; int len, i; char lowch; len = strlen(upperName); assert(len <= A2FileProDOS::kMaxFileName); for (i = 0; i < len; i++) { caseBit >>= 1; lowch = A2FileProDOS::NameToLower(upperName[i]); if (lowch == lowerName[i]) caseMask |= caseBit; } if (forAppleWorks) { uint16_t adjusted; caseMask <<= 1; adjusted = caseMask << 8 | caseMask >> 8; return adjusted; } else { if (caseMask == 0x8000) return 0; // all upper case, don't freak out pre-v1.8 else return caseMask; } } /* * Generate the lower-case version of a ProDOS filename, using the supplied * lower case flags. "lowerName" must be able to hold 15 chars (enough for * a filename or volname). * * The string will NOT be null-terminated, but the output buffer will be padded * with NULs out to the maximum filename len. This makes it suitable for * copying directly into directory block buffers. * * It's okay to pass the same buffer for "upperName" and "lowerName". * * "lcFlags" is either ProDOS directory flags or AppleWorks aux type flags, * depending on the value of "fromAppleWorks". */ /*static*/ void DiskFSProDOS::GenerateLowerCaseName(const char* upperName, char* lowerName, uint16_t lcFlags, bool fromAppleWorks) { int nameLen = strlen(upperName); int bit; assert(nameLen <= A2FileProDOS::kMaxFileName); if (fromAppleWorks) { /* handle AppleWorks lower-case-in-auxtype */ uint16_t caseMask = // swap bytes (lcFlags << 8) | (lcFlags >> 8); for (bit = 0; bit < nameLen ; bit++) { if ((caseMask & 0x8000) != 0) lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]); else lowerName[bit] = upperName[bit]; caseMask <<= 1; } for ( ; bit < A2FileProDOS::kMaxFileName; bit++) lowerName[bit] = '\0'; } else { /* handle lower-case conversion; see GS/OS tech note #8 */ if (lcFlags != 0 && !(lcFlags & 0x8000)) { // Should be zero or 0x8000 plus other bits; shouldn't be // bunch of bits without 0x8000 or 0x8000 by itself. Not // really a problem, just unexpected. assert(false); memcpy(lowerName, upperName, A2FileProDOS::kMaxFileName); return; } for (bit = 0; bit < nameLen; bit++) { lcFlags <<= 1; if ((lcFlags & 0x8000) != 0) lowerName[bit] = A2FileProDOS::NameToLower(upperName[bit]); else lowerName[bit] = upperName[bit]; } } for ( ; bit < A2FileProDOS::kMaxFileName; bit++) lowerName[bit] = '\0'; } /* * Normalize a ProDOS path. Invokes DoNormalizePath and handles the buffer * management (if the normalized path doesn't fit in "*pNormalizedBufLen" * bytes, we set "*pNormalizedBufLen to the required length). * * This is invoked from the generalized "add" function in CiderPress, which * doesn't want to understand the ins and outs of ProDOS pathnames. */ DIError DiskFSProDOS::NormalizePath(const char* path, char fssep, char* normalizedBuf, int* pNormalizedBufLen) { DIError dierr = kDIErrNone; char* normalizedPath = NULL; int len; assert(pNormalizedBufLen != NULL); assert(normalizedBuf != NULL || *pNormalizedBufLen == 0); dierr = DoNormalizePath(path, fssep, &normalizedPath); if (dierr != kDIErrNone) goto bail; assert(normalizedPath != NULL); len = strlen(normalizedPath); if (normalizedBuf == NULL || *pNormalizedBufLen <= len) { /* too short */ dierr = kDIErrDataOverrun; } else { /* fits */ strcpy(normalizedBuf, normalizedPath); } *pNormalizedBufLen = len+1; // alloc room for the '\0' bail: delete[] normalizedPath; return dierr; } /* * Normalize a ProDOS path. This requires separating each path component * out, making it ProDOS-compliant, and then putting it back in. * The fssep could be anything, so we need to change it to kFssep. * * We don't try to identify duplicates here. If more than one subdir maps * to the same thing, then you're just going to end up with lots of files * in the same subdir. If this is unacceptable then it will have to be * fixed at a higher level. * * Lower-case letters and spaces are left in place. They're expected to * be removed later. * * The caller must delete[] "*pNormalizedPath". */ DIError DiskFSProDOS::DoNormalizePath(const char* path, char fssep, char** pNormalizedPath) { DIError dierr = kDIErrNone; char* workBuf = NULL; char* partBuf = NULL; char* outputBuf = NULL; char* start; char* end; char* outPtr; assert(path != NULL); workBuf = new char[strlen(path)+1]; partBuf = new char[strlen(path)+1 +1]; // need +1 for prepending letter outputBuf = new char[strlen(path) * 2]; if (workBuf == NULL || partBuf == NULL || outputBuf == NULL) { dierr = kDIErrMalloc; goto bail; } strcpy(workBuf, path); outputBuf[0] = '\0'; outPtr = outputBuf; start = workBuf; while (*start != '\0') { //char* origStart = start; // need for debug msg int partIdx; if (fssep == '\0') { end = NULL; } else { end = strchr(start, fssep); if (end != NULL) *end = '\0'; } partIdx = 0; /* * Skip over everything up to the first letter. If we encounter a * number or a '\0' first, insert a leading letter. */ while (*start != '\0') { if (toupper(*start) >= 'A' && toupper(*start) <= 'Z') { partBuf[partIdx++] = *start++; break; } if (*start >= '0' && *start <= '9') { partBuf[partIdx++] = 'A'; break; } start++; } if (partIdx == 0) partBuf[partIdx++] = 'Z'; /* * Continue copying, dropping all illegal chars. */ while (*start != '\0') { if ((toupper(*start) >= 'A' && toupper(*start) <= 'Z') || (*start >= '0' && *start <= '9') || (*start == '.') || (*start == ' ') ) { partBuf[partIdx++] = *start++; } else { start++; } } /* * Truncate at 15 chars, preserving anything that looks like a * filename extension. "partIdx" represents the length of the * string at this point. "partBuf" holds the string, which we * want to null-terminate before proceeding. */ partBuf[partIdx] = '\0'; if (partIdx > A2FileProDOS::kMaxFileName) { const char* pDot = strrchr(partBuf, '.'); //int DEBUGDOTLEN = pDot - partBuf; if (pDot != NULL && partIdx - (pDot-partBuf) <= kMaxExtensionLen) { int dotLen = partIdx - (pDot-partBuf); memmove(partBuf + (A2FileProDOS::kMaxFileName - dotLen), pDot, dotLen); // don't use memcpy, move might overlap } partIdx = A2FileProDOS::kMaxFileName; } partBuf[partIdx] = '\0'; //LOGI(" ProDOS Converted component '%s' to '%s'", // origStart, partBuf); if (outPtr != outputBuf) *outPtr++ = A2FileProDOS::kFssep; strcpy(outPtr, partBuf); outPtr += partIdx; /* * Continue with next segment. */ if (end == NULL) break; start = end+1; } *outPtr = '\0'; LOGI(" ProDOS Converted path '%s' to '%s' (fssep='%c')", path, outputBuf, fssep); assert(*outputBuf != '\0'); *pNormalizedPath = outputBuf; outputBuf = NULL; bail: delete[] workBuf; delete[] partBuf; delete[] outputBuf; return dierr; } /* * Create a copy of the filename with everything in upper case and spaces * changed to periods. * * "upperName" must be a buffer that holds at least kMaxFileName+1 characters. * If "name" is longer than kMaxFileName, it will be truncated. */ void DiskFSProDOS::UpperCaseName(char* upperName, const char* name) { int i; for (i = 0; i < A2FileProDOS::kMaxFileName; i++) { char ch = name[i]; if (ch == '\0') break; else if (ch == ' ') upperName[i] = '.'; else upperName[i] = toupper(ch); } /* null terminate with prejudice -- we memcpy this buffer into subdirs */ for ( ; i <= A2FileProDOS::kMaxFileName; i++) upperName[i] = '\0'; } /* * Allocate a new directory entry. We start by reading the entire thing * into memory. If the current set of allocated directory blocks is full, * and we're not operating on the volume dir, we extend the directory. * * This just allocates the space; it does not fill in any details, except * for the prev/next block pointers and the file count in the header. (One * small exception: if we have to extend the directory, the "prev/next" fields * of the new block will be filled in.) * * The volume in-use block map must be loaded before this is called. If * this needs to extend the directory, a new block will be allocated. * * Returns a pointer to the new entry, and a whole bunch of other stuff: * "ppDir" gets a pointer to newly-allocated memory with the whole directory * "pDirLen" is the size of the *ppDir buffer * "ppDirEntry" gets a memory pointer to the start of the created entry * "pDirKeyBlock" gets the key block of the directory as a whole * "pDirEntrySlot" gets the slot number within the directory block (first is 1) * "pDirBlock" gets the actual block in which the created entry resides * * The caller should Write the entire thing to "pOpenSubdir" after filling * in the new details for the entry. * * Possible reasons for failure: disk is out of space, volume dir is out * of space, pOpenSubdir is screwy. * * We guarantee that we will return the first available entry in the current * directory. */ DIError DiskFSProDOS::AllocDirEntry(A2FileDescr* pOpenSubdir, uint8_t** ppDir, long* pDirLen, uint8_t** ppDirEntry, uint16_t* pDirKeyBlock, int* pDirEntrySlot, uint16_t* pDirBlock) { assert(pOpenSubdir != NULL); *ppDirEntry = NULL; *pDirLen = -1; *pDirKeyBlock = 0; *pDirEntrySlot = -1; *pDirBlock = 0; DIError dierr = kDIErrNone; uint8_t* dirBuf = NULL; long dirLen; A2FileProDOS* pFile; long newBlock = -1; /* * Load the subdir into memory. */ pFile = (A2FileProDOS*) pOpenSubdir->GetFile(); dirLen = (long) pFile->GetDataLength(); if (dirLen < 512 || (dirLen % 512) != 0) { LOGI(" ProDOS GLITCH: funky dir EOF %ld (quality=%d)", dirLen, pFile->GetQuality()); dierr = kDIErrBadFile; goto bail; } dirBuf = new uint8_t[dirLen]; if (dirBuf == NULL) { dierr = kDIErrMalloc; goto bail; } dierr = pOpenSubdir->Read(dirBuf, dirLen); if (dierr != kDIErrNone) goto bail; if (dirBuf[0x23] != kEntryLength || dirBuf[0x24] != kEntriesPerBlock) { LOGI(" ProDOS GLITCH: funky entries per block %d", dirBuf[0x24]); dierr = kDIErrBadDirectory; goto bail; } /* * Find the first available entry (storage_type is zero). We need to * step through this by blocks, because the data is block-oriented. * If we run off the end of the last block, (re)alloc a new one. */ uint8_t* pDirEntry; int blockIdx; int entryIdx; pDirEntry = NULL; // make the compiler happy entryIdx = -1; // make the compiler happy for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) { pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next for (entryIdx = 0; entryIdx < kEntriesPerBlock; entryIdx++, pDirEntry += kEntryLength) { if ((pDirEntry[0x00] & 0xf0) == 0) { LOGI(" ProDOS Found empty dir entry in slot %d", entryIdx); break; // found one; break out of inner loop } } if (entryIdx < kEntriesPerBlock) break; // out of outer loop } if (blockIdx == dirLen / 512) { if (((dirBuf[0x04] & 0xf0) >> 4) == A2FileProDOS::kStorageVolumeDirHeader) { /* can't extend the volume dir */ dierr = kDIErrVolumeDirFull; goto bail; } LOGI(" ProDOS ran out of directory space, adding another block"); /* * Request an unused block from the system. Point the "next" pointer * in the last block at it, so that when we go to write this dir * we will know where to put it. */ uint8_t* pBlock; pBlock = dirBuf + 512 * (blockIdx-1); if (pBlock[0x02] != 0) { LOGI(" ProDOS GLITCH: adding to block with nonzero next ptr!"); dierr = kDIErrBadDirectory; goto bail; } newBlock = AllocBlock(); if (newBlock < 0) { dierr = kDIErrDiskFull; goto bail; } PutShortLE(&pBlock[0x02], (uint16_t) newBlock); // set "next" /* * Extend our memory buffer to hold the new entry. */ uint8_t* newSpace = new uint8_t[dirLen + 512]; if (newSpace == NULL) { dierr = kDIErrMalloc; goto bail; } memcpy(newSpace, dirBuf, dirLen); memset(newSpace + dirLen, 0, 512); delete[] dirBuf; dirBuf = newSpace; dirLen += 512; /* * Set the "prev" pointer in the new block to point at the last * block of the existing directory structure. */ long lastBlock; dierr = pOpenSubdir->GetStorage(blockIdx-1, &lastBlock); if (dierr != kDIErrNone) goto bail; pBlock = dirBuf + 512 * blockIdx; PutShortLE(&pBlock[0x00], (uint16_t) lastBlock); // set "prev" assert(GetShortLE(&pBlock[0x02]) == 0); // "next" pointer /* * Finally, point pDirEntry at the first entry in the new area. */ pDirEntry = pBlock + 4; entryIdx = 0; assert(pDirEntry[0x00] == 0x00); } /* * Success. Update the file count in the header. */ uint16_t count; count = GetShortLE(&dirBuf[0x25]); count++; PutShortLE(&dirBuf[0x25], count); long whichBlock; *ppDir = dirBuf; *pDirLen = dirLen; *ppDirEntry = pDirEntry; *pDirKeyBlock = pFile->fDirEntry.keyPointer; *pDirEntrySlot = entryIdx +1; if (blockIdx == ((A2FDProDOS*)pOpenSubdir)->GetBlockCount()) { /* not yet added to block list, so can't use GetStorage */ assert(newBlock > 0); *pDirBlock = (uint16_t) newBlock; } else { assert(newBlock < 0); dierr = pOpenSubdir->GetStorage(blockIdx, &whichBlock); assert(dierr == kDIErrNone); *pDirBlock = (uint16_t) whichBlock; } dirBuf = NULL; bail: delete[] dirBuf; return dierr; } /* * Given a pointer to a directory buffer and a pointer to an entry, find the * previous entry. (This is handy when trying to figure out where to insert * a new entry into the DiskFS linear file list.) * * If the previous entry is the first in the list (i.e. it's a volume or * subdir header), this returns NULL. * * This is a little awkward because the directories are chopped up into * 512-byte blocks, with 13 entries per block (which doesn't completely fill * the block, leaving gaps we have to skip around). If the previous entry is * in the same block we can just return (ptr-0x27), but if it's in a previous * block we need to return the last entry in the previous. */ uint8_t* DiskFSProDOS::GetPrevDirEntry(uint8_t* buf, uint8_t* ptr) { assert(buf != NULL); assert(ptr != NULL); const int kStartOffset = 4; if (ptr == buf + kStartOffset || ptr == buf + kStartOffset + kEntryLength) return NULL; while (ptr - buf > 512) buf += 512; assert((ptr - buf - kStartOffset) % kEntryLength == 0); if (ptr == buf + kStartOffset) { /* whoops, went too far */ buf -= 512; return buf + kStartOffset + kEntryLength * (kEntriesPerBlock-1); } else { return ptr - kEntryLength; } } /* * Make the name pointed to by "fileName" unique within the directory * loaded in "subdirBuf". The name should already be trimmed to 15 chars * or less and converted to upper-case only, and be in a buffer that can * hold at least kMaxFileName+1 bytes. * * Returns an error on failure, which should only happen if there are a * large number of files with similar names. */ DIError DiskFSProDOS::MakeFileNameUnique(const uint8_t* dirBuf, long dirLen, char* fileName) { assert(dirBuf != NULL); assert(dirLen > 0); assert((dirLen % 512) == 0); assert(fileName != NULL); assert(strlen(fileName) <= A2FileProDOS::kMaxFileName); if (!NameExistsInDir(dirBuf, dirLen, fileName)) return kDIErrNone; LOGI(" ProDOS found duplicate of '%s', making unique", fileName); int nameLen = strlen(fileName); int dotOffset=0, dotLen=0; char dotBuf[kMaxExtensionLen+1]; /* ensure the result will be null-terminated */ memset(fileName + nameLen, 0, (A2FileProDOS::kMaxFileName - nameLen) +1); /* * If this has what looks like a filename extension, grab it. We want * to preserve ".gif", ".c", etc., since the filetypes don't necessarily * do everything we need. * * This will tend to screw up the upper/lower case stuff, especially * since what we think is a '.' might actually be a ' '. We could work * around this, but it's probably not necessary. */ const char* cp = strrchr(fileName, '.'); if (cp != NULL) { int tmpOffset = cp - fileName; if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { LOGI(" ProDOS (keeping extension '%s')", cp); assert(strlen(cp) <= kMaxExtensionLen); strcpy(dotBuf, cp); dotOffset = tmpOffset; dotLen = nameLen - dotOffset; } } const int kMaxDigits = 999; int digits = 0; int digitLen; int copyOffset; char digitBuf[4]; do { if (digits == kMaxDigits) return kDIErrFileExists; digits++; /* not the most efficient way to do this, but it'll do */ sprintf(digitBuf, "%d", digits); digitLen = strlen(digitBuf); if (nameLen + digitLen > A2FileProDOS::kMaxFileName) copyOffset = A2FileProDOS::kMaxFileName - dotLen - digitLen; else copyOffset = nameLen - dotLen; memcpy(fileName + copyOffset, digitBuf, digitLen); if (dotLen != 0) memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); } while (NameExistsInDir(dirBuf, dirLen, fileName)); LOGI(" ProDOS converted to unique name: %s", fileName); return kDIErrNone; } /* * Determine whether the specified file name exists in the raw directory * buffer. * * This should be called with the upper-case-only version of the filename. */ bool DiskFSProDOS::NameExistsInDir(const uint8_t* dirBuf, long dirLen, const char* fileName) { const uint8_t* pDirEntry; int blockIdx; int entryIdx; int nameLen = strlen(fileName); assert(nameLen <= A2FileProDOS::kMaxFileName); for (blockIdx = 0; blockIdx < dirLen / 512; blockIdx++) { pDirEntry = dirBuf + 512*blockIdx + 4; // skip 4 bytes of prev/next for (entryIdx = 0; entryIdx < kEntriesPerBlock; entryIdx++, pDirEntry += kEntryLength) { /* skip directory header */ if (blockIdx == 0 && entryIdx == 0) continue; if ((pDirEntry[0x00] & 0xf0) != 0 && (pDirEntry[0x00] & 0x0f) == nameLen && strncmp((char*) &pDirEntry[0x01], fileName, nameLen) == 0) { return true; } } } return false; } /* * Delete a file. * * There are three fairly simple steps: (1) mark all blocks used by the file as * free, (2) set the storage type in the directory entry to 0, and (3) * decrement the file count in the directory header. We then remove it from * the DiskFS file list. * * We only allow deletion of a subdirectory when the subdir is empty. */ DIError DiskFSProDOS::DeleteFile(A2File* pGenericFile) { DIError dierr = kDIErrNone; long blockCount = -1; long indexCount = -1; uint16_t* blockList = NULL; uint16_t* indexList = NULL; if (pGenericFile == NULL) { assert(false); return kDIErrInvalidArg; } if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; if (pGenericFile->IsFileOpen()) return kDIErrFileOpen; /* * If they try to delete all entries, we don't want to spit back a * failure message over our "fake" volume dir entry. So we just silently * ignore the request. */ if (pGenericFile->IsVolumeDirectory()) { LOGI("ProDOS not deleting volume directory"); return kDIErrNone; } A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; LOGI(" Deleting '%s'", pFile->GetPathName()); dierr = LoadVolBitmap(); if (dierr != kDIErrNone) goto bail; switch (pFile->fDirEntry.storageType) { case A2FileProDOS::kStorageExtended: // handle rsrc fork here, fall out for data fork dierr = pFile->LoadBlockList( pFile->fExtRsrc.storageType, pFile->fExtRsrc.keyBlock, pFile->fExtRsrc.eof, &blockCount, &blockList, &indexCount, &indexList); if (dierr != kDIErrNone) goto bail; FreeBlocks(blockCount, blockList); if (indexList != NULL) // no indices for seedling FreeBlocks(indexCount, indexList); delete[] blockList; delete[] indexList; indexList = NULL; // handle the key block "manually" blockCount = 1; blockList = new uint16_t[blockCount]; blockList[0] = pFile->fDirEntry.keyPointer; FreeBlocks(blockCount, blockList); delete[] blockList; blockList = NULL; dierr = pFile->LoadBlockList( pFile->fExtData.storageType, pFile->fExtData.keyBlock, pFile->fExtData.eof, &blockCount, &blockList, &indexCount, &indexList); break; // fall out case A2FileProDOS::kStorageDirectory: dierr = pFile->LoadDirectoryBlockList( pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof, &blockCount, &blockList); break; // fall out case A2FileProDOS::kStorageSeedling: case A2FileProDOS::kStorageSapling: case A2FileProDOS::kStorageTree: dierr = pFile->LoadBlockList( pFile->fDirEntry.storageType, pFile->fDirEntry.keyPointer, pFile->fDirEntry.eof, &blockCount, &blockList, &indexCount, &indexList); break; // fall out default: LOGI("ProDOS can't delete unknown storage type %d", pFile->fDirEntry.storageType); dierr = kDIErrBadDirectory; break; // fall out } if (dierr != kDIErrNone) goto bail; FreeBlocks(blockCount, blockList); if (indexList != NULL) FreeBlocks(indexCount, indexList); /* * Update the directory entry. After this point, failure gets ugly. * * It might be "proper" to open the subdir file, find the correct entry, * and write it back, but the A2FileProDOS structure has the directory * block and entry index stored in it. Makes it a little easier. */ uint8_t blkBuf[kBlkSize]; uint8_t* ptr; assert(pFile->fParentDirBlock > 0); assert(pFile->fParentDirIdx >= 0 && pFile->fParentDirIdx < kEntriesPerBlock); dierr = fpImg->ReadBlock(pFile->fParentDirBlock, blkBuf); if (dierr != kDIErrNone) { LOGI("ProDOS unable to read directory block %u", pFile->fParentDirBlock); goto bail; } ptr = blkBuf + 4 + pFile->fParentDirIdx * kEntryLength; if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)", (*ptr) >> 4, pFile->fDirEntry.storageType); assert(false); dierr = kDIErrBadDirectory; goto bail; } ptr[0x00] = 0; // zap both storage type and name length dierr = fpImg->WriteBlock(pFile->fParentDirBlock, blkBuf); if (dierr != kDIErrNone) { LOGI("ProDOS unable to write directory block %u", pFile->fParentDirBlock); goto bail; } /* * Save our updated copy of the volume bitmap to disk. */ dierr = SaveVolBitmap(); if (dierr != kDIErrNone) goto bail; /* * One last little thing: decrement the file count in the directory * header. We can find the appropriate place pretty easily because * we know it's the first block in pFile->fpParent, which for a dir is * always the block pointed to by the key pointer. * * Strictly speaking, failure to update this correctly isn't fatal. I * doubt most utilities pay any attention to this. Still, it's important * to keep the filesystem in a consistent state, so we at least must * report the error. They'll need to run the ProSel volume repair util * to fix it. */ A2FileProDOS* pParent; uint16_t fileCount; int storageType; pParent = (A2FileProDOS*) pFile->GetParent(); assert(pParent != NULL); assert(pParent->fDirEntry.keyPointer >= kVolHeaderBlock); dierr = fpImg->ReadBlock(pParent->fDirEntry.keyPointer, blkBuf); if (dierr != kDIErrNone) { LOGI("ProDOS unable to read parent dir block %u", pParent->fDirEntry.keyPointer); goto bail; } ptr = NULL; storageType = (blkBuf[0x04] & 0xf0) >> 4; if (storageType != A2FileProDOS::kStorageSubdirHeader && storageType != A2FileProDOS::kStorageVolumeDirHeader) { LOGI("ProDOS invalid storage type %d in dir header block", storageType); DebugBreak(); dierr = kDIErrBadDirectory; goto bail; } fileCount = GetShortLE(&blkBuf[0x25]); if (fileCount > 0) fileCount--; PutShortLE(&blkBuf[0x25], fileCount); dierr = fpImg->WriteBlock(pParent->fDirEntry.keyPointer, blkBuf); if (dierr != kDIErrNone) { LOGI("ProDOS unable to write parent dir block %u", pParent->fDirEntry.keyPointer); goto bail; } /* * Remove the A2File* from the list. */ DeleteFileFromList(pFile); bail: FreeVolBitmap(); delete[] blockList; delete[] indexList; return kDIErrNone; } /* * Mark all of the blocks in the blockList as free. * * The in-use map must already be loaded. */ DIError DiskFSProDOS::FreeBlocks(long blockCount, uint16_t* blockList) { VolumeUsage::ChunkState cstate; int i; //LOGI(" +++ FreeBlocks (blockCount=%d blockList=0x%08lx)", // blockCount, blockList); assert(blockCount >= 0 && blockCount < 65536); assert(blockList != NULL); cstate.isUsed = false; cstate.isMarkedUsed = false; cstate.purpose = VolumeUsage::kChunkPurposeUnknown; for (i = 0; i < blockCount; i++) { if (blockList[i] == 0) // expected for "sparse" files continue; if (!GetBlockUseEntry(blockList[i])) { LOGI("WARNING: freeing unallocated block %u", blockList[i]); assert(false); // impossible unless disk is "damaged" } SetBlockUseEntry(blockList[i], false); fVolumeUsage.SetChunkState(blockList[i], &cstate); } return kDIErrNone; } /* * Rename a file. * * Pass in a pointer to the file and a string with the new filename (just * the filename, not a pathname -- this function doesn't move files * between directories). The new name must already be normalized. * * Renaming the magic volume directory "file" is not allowed. * * Things to note: * - Renaming subdirs is annoying. The name has to be changed in two * places, and the "pathname" value cached in A2FileProDOS must be * updated for all children of the subdir. * - Must check for duplicates. * - If it's an AppleWorks file type, we need to change the aux type * according to the upper/lower case flags. This holds even if the * "allow lower case" flag is disabled. */ DIError DiskFSProDOS::RenameFile(A2File* pGenericFile, const char* newName) { DIError dierr = kDIErrNone; A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; char upperName[A2FileProDOS::kMaxFileName+1]; char upperComp[A2FileProDOS::kMaxFileName+1]; if (pFile == NULL || newName == NULL) return kDIErrInvalidArg; if (!IsValidFileName(newName)) return kDIErrInvalidArg; if (pFile->IsVolumeDirectory()) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; LOGI(" ProDOS renaming '%s' to '%s'", pFile->GetPathName(), newName); /* * Check for duplicates. We do this by getting the parent subdir and * running through it looking for an upper-case-converted match. * * We start in the list at our parent node, knowing that the kids are * grouped together after it. However, we can't stop right away, * because some of the kids might be subdirectories themselves. So we * will probably run through a significant chunk of the list. */ A2File* pParent = pFile->GetParent(); A2File* pCur; UpperCaseName(upperName, newName); pCur = GetNextFile(pParent); assert(pCur != NULL); // at the very least, pFile is in this dir while (pCur != NULL) { if (pCur != pFile && pCur->GetParent() == pParent) { /* one of our siblings; see if the name matches */ UpperCaseName(upperComp, pCur->GetFileName()); if (strcmp(upperName, upperComp) == 0) { LOGI(" ProDOS rename dup found"); return kDIErrFileExists; } } pCur = GetNextFile(pCur); } /* * Grab the directory block and update the filename in the entry. If this * was a subdir we also need to update its directory header entry. To * minimize the chances of a partial update, we load both blocks up * front, modify both, then write them both back. */ uint8_t parentDirBuf[kBlkSize]; uint8_t thisDirBuf[kBlkSize]; dierr = fpImg->ReadBlock(pFile->fParentDirBlock, parentDirBuf); if (dierr != kDIErrNone) goto bail; if (pFile->IsDirectory()) { dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf); if (dierr != kDIErrNone) goto bail; } /* compute lower case flags as needed */ uint16_t lcFlags, lcAuxType; bool allowLowerCase, isAW; allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0; isAW = A2FileProDOS::UsesAppleWorksAuxType((uint8_t)pFile->GetFileType()); if (allowLowerCase) lcFlags = GenerateLowerCaseBits(upperName, newName, false); else lcFlags = 0; if (isAW) lcAuxType = GenerateLowerCaseBits(upperName, newName, true); else lcAuxType = 0; /* * Possible optimization: if "upperName" matches what's in the block on * disk and the "lcFlags"/"lcAuxType" values match as well, we don't * need to write the blocks back. * * It's difficult to test for this earlier, because we need to do the * update if (a) they're just changing the capitalization or (b) we're * changing the capitalization for them because the "allow lower case" * flag got turned off. */ /* find the right entry, and copy our filename in */ uint8_t* ptr; assert(pFile->fParentDirIdx >= 0 && pFile->fParentDirIdx < kEntriesPerBlock); ptr = parentDirBuf + 4 + pFile->fParentDirIdx * kEntryLength; if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)", (*ptr) >> 4, pFile->fDirEntry.storageType); assert(false); dierr = kDIErrBadDirectory; goto bail; } ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName)); memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); PutShortLE(&ptr[0x1c], lcFlags); // version/min_version if (isAW) PutShortLE(&ptr[0x1f], lcAuxType); if (pFile->IsDirectory()) { ptr = thisDirBuf + 4; if ((*ptr) >> 4 != A2FileProDOS::kStorageSubdirHeader) { LOGI("ProDOS GLITCH: bad storage type in subdir header (%d)", (*ptr) >> 4); assert(false); dierr = kDIErrBadDirectory; goto bail; } ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName)); memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); PutShortLE(&ptr[0x1c], lcFlags); // version/min_version } /* write the updated data back to the disk */ dierr = fpImg->WriteBlock(pFile->fParentDirBlock, parentDirBuf); if (dierr != kDIErrNone) goto bail; if (pFile->IsDirectory()) { dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf); if (dierr != kDIErrNone) goto bail; } /* * At this point the ProDOS filesystem is back in a consistent state. * Everything we do from here on is self-inflicted. * * We need to update this entry's A2FileProDOS::fDirEntry.fileName, * as well as the A2FileProDOS::fPathName. If this was a subdir, then * we need to update A2FileProDOS::fPathName for all files inside the * directory (including children of children). * * The latter is somewhat awkward, so we just re-acquire the pathname * for every file on the disk. Less efficient but easier to code. */ if (isAW) GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName, lcAuxType, true); else GenerateLowerCaseName(upperName, pFile->fDirEntry.fileName, lcFlags, false); assert(pFile->fDirEntry.fileName[A2FileProDOS::kMaxFileName] == '\0'); if (pFile->IsDirectory()) { /* do all files that come after us */ pCur = pFile; while (pCur != NULL) { RegeneratePathName((A2FileProDOS*) pCur); pCur = GetNextFile(pCur); } } else { RegeneratePathName(pFile); } LOGI("Okay!"); bail: return dierr; } /* * Regenerate fPathName for the specified file. * * Has no effect on the magic volume dir entry. * * This could be implemented more efficiently, but it's only used when * renaming files, so there's not much point. */ DIError DiskFSProDOS::RegeneratePathName(A2FileProDOS* pFile) { A2FileProDOS* pParent; char* buf = NULL; int len; /* nothing to do here */ if (pFile->IsVolumeDirectory()) return kDIErrNone; /* compute the length of the path name */ len = strlen(pFile->GetFileName()); pParent = (A2FileProDOS*) pFile->GetParent(); while (!pParent->IsVolumeDirectory()) { len++; // leave space for the ':' len += strlen(pParent->GetFileName()); pParent = (A2FileProDOS*) pParent->GetParent(); } buf = new char[len+1]; if (buf == NULL) return kDIErrMalloc; /* generate the new path name */ int partLen; partLen = strlen(pFile->GetFileName()); strcpy(buf + len - partLen, pFile->GetFileName()); len -= partLen; pParent = (A2FileProDOS*) pFile->GetParent(); while (!pParent->IsVolumeDirectory()) { assert(len > 0); buf[--len] = kDIFssep; partLen = strlen(pParent->GetFileName()); strncpy(buf + len - partLen, pParent->GetFileName(), partLen); len -= partLen; assert(len >= 0); pParent = (A2FileProDOS*) pParent->GetParent(); } LOGI("Replacing '%s' with '%s'", pFile->GetPathName(), buf); pFile->SetPathName("", buf); delete[] buf; return kDIErrNone; } /* * Change the attributes of the specified file. * * Subdirectories have access bits in the subdir header as well as their * file entry. The BASIC.SYSTEM "lock" command only changes the access * bits of the file; the permissions inside the subdir remain 0xe3. (Which * might explain why you can still add files to a locked subdir.) I'm going * to mimic this behavior. * * This does, of course, mean that there's no meaning in attempts to change * the file access permissions of the volume directory. */ DIError DiskFSProDOS::SetFileInfo(A2File* pGenericFile, uint32_t fileType, uint32_t auxType, uint32_t accessFlags) { DIError dierr = kDIErrNone; A2FileProDOS* pFile = (A2FileProDOS*) pGenericFile; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (pFile == NULL) { assert(false); return kDIErrInvalidArg; } if ((fileType & ~(0xff)) != 0 || (auxType & ~(0xffff)) != 0 || (accessFlags & ~(0xff)) != 0) { return kDIErrInvalidArg; } if (pFile->IsVolumeDirectory()) { LOGI(" ProDOS refusing to change file info for volume dir"); return kDIErrAccessDenied; // not quite right } LOGI("ProDOS changing values for '%s' to 0x%02x 0x%04x 0x%02x", pFile->GetPathName(), fileType, auxType, accessFlags); /* load the directory block for this file */ uint8_t thisDirBuf[kBlkSize]; dierr = fpImg->ReadBlock(pFile->fParentDirBlock, thisDirBuf); if (dierr != kDIErrNone) goto bail; /* find the right entry, and set the fields */ uint8_t* ptr; assert(pFile->fParentDirIdx >= 0 && pFile->fParentDirIdx < kEntriesPerBlock); ptr = thisDirBuf + 4 + pFile->fParentDirIdx * kEntryLength; if ((*ptr) >> 4 != pFile->fDirEntry.storageType) { LOGI("ProDOS GLITCH: mismatched storage types (%d vs %d)", (*ptr) >> 4, pFile->fDirEntry.storageType); assert(false); dierr = kDIErrBadDirectory; goto bail; } if ((size_t) (*ptr & 0x0f) != strlen(pFile->fDirEntry.fileName)) { LOGW("ProDOS GLITCH: wrong file? (len=%d vs %u)", *ptr & 0x0f, (unsigned int) strlen(pFile->fDirEntry.fileName)); assert(false); dierr = kDIErrBadDirectory; goto bail; } ptr[0x10] = (uint8_t) fileType; ptr[0x1e] = (uint8_t) accessFlags; PutShortLE(&ptr[0x1f], (uint16_t) auxType); dierr = fpImg->WriteBlock(pFile->fParentDirBlock, thisDirBuf); if (dierr != kDIErrNone) goto bail; /* update our local copy */ pFile->fDirEntry.fileType = (uint8_t) fileType; pFile->fDirEntry.auxType = (uint16_t) auxType; pFile->fDirEntry.access = (uint8_t) accessFlags; bail: return dierr; } /* * Change the disk volume name. * * This is a lot like renaming a subdirectory, except that there's no parent * directory to update, and the name of the volume dir doesn't affect the * pathname of anything else. There's also no risk of a duplicate. * * Internally we need to update the "fake" entry and the cached copies in * fVolumeName and fVolumeID. */ DIError DiskFSProDOS::RenameVolume(const char* newName) { DIError dierr = kDIErrNone; char upperName[A2FileProDOS::kMaxFileName+1]; A2FileProDOS* pFile; if (!IsValidVolumeName(newName)) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; pFile = (A2FileProDOS*) GetNextFile(NULL); assert(pFile != NULL); assert(strcmp(pFile->GetFileName(), fVolumeName) == 0); LOGI(" ProDOS renaming volume '%s' to '%s'", pFile->GetPathName(), newName); /* * Figure out the lower-case flags. */ uint16_t lcFlags; bool allowLowerCase; UpperCaseName(upperName, newName); allowLowerCase = GetParameter(kParmProDOS_AllowLowerCase) != 0; if (allowLowerCase) lcFlags = GenerateLowerCaseBits(upperName, newName, false); else lcFlags = 0; /* * Update the volume dir header. */ uint8_t thisDirBuf[kBlkSize]; uint8_t* ptr; assert(pFile->fDirEntry.keyPointer == kVolHeaderBlock); dierr = fpImg->ReadBlock(pFile->fDirEntry.keyPointer, thisDirBuf); if (dierr != kDIErrNone) goto bail; ptr = thisDirBuf + 4; if ((*ptr) >> 4 != A2FileProDOS::kStorageVolumeDirHeader) { LOGI("ProDOS GLITCH: bad storage type in voldir header (%d)", (*ptr) >> 4); assert(false); dierr = kDIErrBadDirectory; goto bail; } ptr[0x00] = (uint8_t)((ptr[0x00] & 0xf0) | strlen(upperName)); memcpy(&ptr[0x01], upperName, A2FileProDOS::kMaxFileName); PutShortLE(&ptr[0x16], lcFlags); // reserved fields dierr = fpImg->WriteBlock(pFile->fDirEntry.keyPointer, thisDirBuf); if (dierr != kDIErrNone) goto bail; /* * Set the volume name, based on the upper-case name and lower-case flags * we just wrote. If "allowLowerCase" was set to false, it may not be * the same as what's in "newName". */ char lowerName[A2FileProDOS::kMaxFileName+1]; memset(lowerName, 0, sizeof(lowerName)); // lowerName won't be term'ed GenerateLowerCaseName(upperName, lowerName, lcFlags, false); strcpy(fVolumeName, lowerName); SetVolumeID(); strcpy(pFile->fDirEntry.fileName, lowerName); /* update the entry in the linear file list */ pFile->SetPathName(":", fVolumeName); bail: return dierr; } /* * =========================================================================== * A2FileProDOS * =========================================================================== */ /* * Convert from ProDOS compact date format to a time_t. * * Byte 0 and 1: yyyyyyymmmmddddd * Byte 2 and 3: 000hhhhh00mmmmmm * * The field is set entirely to zero if no date was assigned (which cannot * be a valid date since "day" ranges from 1 to 31). If this is found then * ((time_t) 0) is returned. */ /*static*/ time_t A2FileProDOS::ConvertProDate(ProDate proDate) { uint16_t prodosDate, prodosTime; int year, month, day, hour, minute, second; if (proDate == 0) return 0; prodosDate = (uint16_t) (proDate & 0x0000ffff); prodosTime = (uint16_t) ((proDate >> 16) & 0x0000ffff); second = 0; minute = prodosTime & 0x3f; hour = (prodosTime >> 8) & 0x1f; day = prodosDate & 0x1f; month = (prodosDate >> 5) & 0x0f; year = (prodosDate >> 9) & 0x7f; if (year < 40) year += 100; /* P8 uses 0-39 for 2000-2039 */ struct tm tmbuf; time_t when; tmbuf.tm_sec = second; tmbuf.tm_min = minute; tmbuf.tm_hour = hour; tmbuf.tm_mday = day; tmbuf.tm_mon = month-1; // ProDOS uses 1-12 tmbuf.tm_year = year; tmbuf.tm_wday = 0; tmbuf.tm_yday = 0; tmbuf.tm_isdst = -1; // let it figure DST and time zone when = mktime(&tmbuf); if (when == (time_t) -1) when = 0; return when; } /* * Convert a time_t to a ProDOS-format date. * * CiderPress uses kDateInvalid==-1 and kDateNone==-2. */ /*static*/ A2FileProDOS::ProDate A2FileProDOS::ConvertProDate(time_t unixDate) { ProDate proDate; uint32_t prodosDate, prodosTime; struct tm* ptm; int year; if (unixDate == 0 || unixDate == -1 || unixDate == -2) return 0; ptm = localtime(&unixDate); if (ptm == NULL) return 0; // must've been invalid or unspecified year = ptm->tm_year; #ifdef OLD_PRODOS_DATES /* ProSel-16 volume repair complaints about dates < 1980 and >= Y2K */ if (year > 100) year -= 20; #endif if (year >= 100) year -= 100; if (year < 0 || year >= 128) { LOGI("WHOOPS: got year %d from %d", year, ptm->tm_year); year = 70; } prodosDate = year << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; prodosTime = ptm->tm_hour << 8 | ptm->tm_min; proDate = prodosTime << 16 | prodosDate; return proDate; } /* * Return the file creation time as a time_t. */ time_t A2FileProDOS::GetCreateWhen(void) const { return ConvertProDate(fDirEntry.createWhen); } /* * Return the file modification time as a time_t. */ time_t A2FileProDOS::GetModWhen(void) const { return ConvertProDate(fDirEntry.modWhen); } /* * Set the full pathname to a combination of the base path and the * current file's name. * * If we're in the volume directory, pass in "" for the base path (not NULL). */ void A2FileProDOS::SetPathName(const char* basePath, const char* fileName) { assert(basePath != NULL && fileName != NULL); if (fPathName != NULL) delete[] fPathName; int baseLen = strlen(basePath); fPathName = new char[baseLen + 1 + strlen(fileName)+1]; strcpy(fPathName, basePath); if (baseLen != 0 && !(baseLen == 1 && basePath[0] == ':')) { *(fPathName + baseLen) = kFssep; baseLen++; } strcpy(fPathName + baseLen, fileName); } /* * Convert a character in a ProDOS name to lower case. * * This is special in that '.' is considered upper case, with ' ' as its * lower-case counterpart. */ /*static*/ char A2FileProDOS::NameToLower(char ch) { if (ch == '.') return ' '; else return tolower(ch); } /* * Init the fields in the DirEntry struct from the values in the ProDOS * directory entry pointed to by "entryBuf". * * Deals with lower case conversions on the filename. */ /*static*/ void A2FileProDOS::InitDirEntry(A2FileProDOS::DirEntry* pEntry, const uint8_t* entryBuf) { int nameLen; pEntry->storageType = (entryBuf[0x00] & 0xf0) >> 4; nameLen = entryBuf[0x00] & 0x0f; memcpy(pEntry->fileName, &entryBuf[0x01], nameLen); pEntry->fileName[nameLen] = '\0'; pEntry->fileType = entryBuf[0x10]; pEntry->keyPointer = GetShortLE(&entryBuf[0x11]); pEntry->blocksUsed = GetShortLE(&entryBuf[0x13]); pEntry->eof = GetLongLE(&entryBuf[0x15]); pEntry->eof &= 0x00ffffff; pEntry->createWhen = GetLongLE(&entryBuf[0x18]); pEntry->version = entryBuf[0x1c]; pEntry->minVersion = entryBuf[0x1d]; pEntry->access = entryBuf[0x1e]; pEntry->auxType = GetShortLE(&entryBuf[0x1f]); pEntry->modWhen = GetLongLE(&entryBuf[0x21]); pEntry->headerPointer = GetShortLE(&entryBuf[0x25]); /* generate the name into the buffer; does not null-terminate */ if (UsesAppleWorksAuxType(pEntry->fileType)) { DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName, pEntry->auxType, true); } else if (pEntry->minVersion & 0x80) { DiskFSProDOS::GenerateLowerCaseName(pEntry->fileName, pEntry->fileName, GetShortLE(&entryBuf[0x1c]), false); } pEntry->fileName[sizeof(pEntry->fileName)-1] = '\0'; } /* * Open one fork of this file. * * I really, really dislike forked files. */ DIError A2FileProDOS::Open(A2FileDescr** ppOpenFile, bool readOnly, bool rsrcFork /*= false*/) { DIError dierr = kDIErrNone; A2FDProDOS* pOpenFile = NULL; LOGI(" ProDOS Open(ro=%d, rsrc=%d) on '%s'", readOnly, rsrcFork, fPathName); //Dump(); if (!readOnly) { if (fpDiskFS->GetDiskImg()->GetReadOnly()) return kDIErrAccessDenied; if (fpDiskFS->GetFSDamaged()) return kDIErrBadDiskImage; } if (fpOpenFile != NULL) { dierr = kDIErrAlreadyOpen; goto bail; } if (rsrcFork && fDirEntry.storageType != kStorageExtended) { dierr = kDIErrForkNotFound; goto bail; } pOpenFile = new A2FDProDOS(this); if (pOpenFile == NULL) return kDIErrMalloc; pOpenFile->fOpenRsrcFork = false; if (fDirEntry.storageType == kStorageExtended) { if (rsrcFork) { dierr = LoadBlockList(fExtRsrc.storageType, fExtRsrc.keyBlock, fExtRsrc.eof, &pOpenFile->fBlockCount, &pOpenFile->fBlockList); pOpenFile->fOpenEOF = fExtRsrc.eof; pOpenFile->fOpenBlocksUsed = fExtRsrc.blocksUsed; pOpenFile->fOpenStorageType = fExtRsrc.storageType; pOpenFile->fOpenRsrcFork = true; } else { dierr = LoadBlockList(fExtData.storageType, fExtData.keyBlock, fExtData.eof, &pOpenFile->fBlockCount, &pOpenFile->fBlockList); pOpenFile->fOpenEOF = fExtData.eof; pOpenFile->fOpenBlocksUsed = fExtData.blocksUsed; pOpenFile->fOpenStorageType = fExtData.storageType; } } else if (fDirEntry.storageType == kStorageDirectory || fDirEntry.storageType == kStorageVolumeDirHeader) { dierr = LoadDirectoryBlockList(fDirEntry.keyPointer, fDirEntry.eof, &pOpenFile->fBlockCount, &pOpenFile->fBlockList); pOpenFile->fOpenEOF = fDirEntry.eof; pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed; pOpenFile->fOpenStorageType = fDirEntry.storageType; } else if (fDirEntry.storageType == kStorageSeedling || fDirEntry.storageType == kStorageSapling || fDirEntry.storageType == kStorageTree) { dierr = LoadBlockList(fDirEntry.storageType, fDirEntry.keyPointer, fDirEntry.eof, &pOpenFile->fBlockCount, &pOpenFile->fBlockList); pOpenFile->fOpenEOF = fDirEntry.eof; pOpenFile->fOpenBlocksUsed = fDirEntry.blocksUsed; pOpenFile->fOpenStorageType = fDirEntry.storageType; } else { LOGI("PrODOS can't open unknown storage type %d", fDirEntry.storageType); dierr = kDIErrBadDirectory; goto bail; } if (dierr != kDIErrNone) { LOGI(" ProDOS open failed"); goto bail; } pOpenFile->fOffset = 0; //pOpenFile->DumpBlockList(); fpOpenFile = pOpenFile; // add it to our single-member "open file set" *ppOpenFile = pOpenFile; pOpenFile = NULL; bail: delete pOpenFile; return dierr; } /* * Gather a linear, non-sparse list of file blocks into an array. * * Pass in the storage type and top-level key block. Separation of * extended files should have been handled by the caller. This loads the * list for only one fork. * * There are two kinds of sparse: sparse *inside* data, and sparse * *past* data. The latter is interesting, because there is no need * to create space in index blocks to hold it. Thus, a sapling could * hold a file with an EOF of 16MB. * * If "pIndexBlockCount" and "pIndexBlockList" are non-NULL, then we * also accumulate the list of index blocks and return those as well. * For a Tree-structured file, the first entry in the index list is * the master index block. * * The caller must delete[] "*pBlockList" and "*pIndexBlockList". */ DIError A2FileProDOS::LoadBlockList(int storageType, uint16_t keyBlock, long eof, long* pBlockCount, uint16_t** pBlockList, long* pIndexBlockCount, uint16_t** pIndexBlockList) { if (storageType == kStorageDirectory || storageType == kStorageVolumeDirHeader) { assert(pIndexBlockList == NULL && pIndexBlockCount == NULL); return LoadDirectoryBlockList(keyBlock, eof, pBlockCount, pBlockList); } assert(keyBlock != 0); assert(pBlockCount != NULL); assert(pBlockList != NULL); assert(*pBlockList == NULL); if (storageType != kStorageSeedling && storageType != kStorageSapling && storageType != kStorageTree) { /* * We can get here if somebody puts a bad storage type inside the * extended key block of a forked file. Bad storage types on other * kinds of files are caught earlier. */ LOGI(" ProDOS unexpected storageType %d in '%s'", storageType, GetPathName()); return kDIErrNotSupported; } DIError dierr = kDIErrNone; uint16_t* list = NULL; long count; assert(eof < 1024*1024*16); count = (eof + kBlkSize -1) / kBlkSize; if (count == 0) count = 1; list = new uint16_t[count+1]; if (list == NULL) { dierr = kDIErrMalloc; goto bail; } if (pIndexBlockList != NULL) { assert(pIndexBlockCount != NULL); assert(*pIndexBlockList == NULL); } /* this should take care of trailing sparse entries */ memset(list, 0, sizeof(uint16_t) * count); list[count] = kInvalidBlockNum; // overrun check if (storageType == kStorageSeedling) { list[0] = keyBlock; if (pIndexBlockList != NULL) { *pIndexBlockCount = 0; *pIndexBlockList = NULL; } } else if (storageType == kStorageSapling) { dierr = LoadIndexBlock(keyBlock, list, count); if (dierr != kDIErrNone) goto bail; if (pIndexBlockList != NULL) { *pIndexBlockCount = 1; *pIndexBlockList = new uint16_t[1]; **pIndexBlockList = keyBlock; } } else if (storageType == kStorageTree) { uint8_t blkBuf[kBlkSize]; uint16_t* listPtr = list; uint16_t* outIndexPtr = NULL; long countDown = count; int idx = 0; dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; if (pIndexBlockList != NULL) { int numIndices = (count + kMaxBlocksPerIndex-1) / kMaxBlocksPerIndex; numIndices++; // add one for the master index block *pIndexBlockList = new uint16_t[numIndices]; outIndexPtr = *pIndexBlockList; *outIndexPtr++ = keyBlock; *pIndexBlockCount = 1; } while (countDown) { long blockCount = countDown; if (blockCount > kMaxBlocksPerIndex) blockCount = kMaxBlocksPerIndex; uint16_t idxBlock; idxBlock = blkBuf[idx] | (uint16_t) blkBuf[idx+256] << 8; if (idxBlock == 0) { /* fully sparse index block */ //LOGI(" ProDOS that's seriously sparse (%d)!", idx); memset(listPtr, 0, blockCount * sizeof(uint16_t)); if (pIndexBlockList != NULL) { *outIndexPtr++ = idxBlock; (*pIndexBlockCount)++; } } else { dierr = LoadIndexBlock(idxBlock, listPtr, blockCount); if (dierr != kDIErrNone) goto bail; if (pIndexBlockList != NULL) { *outIndexPtr++ = idxBlock; (*pIndexBlockCount)++; } } idx++; listPtr += blockCount; countDown -= blockCount; } } else { assert(false); } assert(list[count] == kInvalidBlockNum); dierr = ValidateBlockList(list, count); if (dierr != kDIErrNone) goto bail; *pBlockCount = count; *pBlockList = list; bail: if (dierr != kDIErrNone) { delete[] list; assert(*pBlockList == NULL); if (pIndexBlockList != NULL && *pIndexBlockList != NULL) { delete[] *pIndexBlockList; *pIndexBlockList = NULL; } } return dierr; } /* * Make sure all values in the block list fall in accepted ranges. * * We allow zero (used for sparse blocks), but disallow values in the "system" * area (block 1 through the end of the usage map). * * It's hard to say whether we should compare against the DiskImg block count * (representing blocks we can physically read but aren't necessarily part * of the filesystem) or the filesystem "total blocks" value from the volume * header. Using the one in the volume header is correct, but sometimes the * value is off on an otherwise reasonable disk. * * I'm falling on the side of generosity, allowing files that reference * potentially bad data to appear okay. My main reason is that, except for * CFFA volumes that have been tweaked by CiderPress users, very few ProDOS * disks will have a large disparity between the two numbers unless somebody * has trashed the volume dir header. * * What we really need is three states for each file: good, suspect, damaged. */ DIError A2FileProDOS::ValidateBlockList(const uint16_t* list, long count) { DiskImg* pImg = fpDiskFS->GetDiskImg(); bool foundBad = false; while (count--) { if (*list > pImg->GetNumBlocks() || (*list > 0 && *list <= 2)) // not enough, but it'll do { LOGI("Invalid block %d in '%s'", *list, fDirEntry.fileName); SetQuality(kQualityDamaged); return kDIErrBadFile; } if (*list > fpDiskFS->GetFSNumBlocks()) foundBad = true; list++; } if (foundBad) { LOGI(" --- found out-of-range block in '%s'", GetPathName()); SetQuality(kQualitySuspicious); } return kDIErrNone; } /* * Copy the entries from the index block in "block" to "list", copying * at most "maxCount" entries. */ DIError A2FileProDOS::LoadIndexBlock(uint16_t block, uint16_t* list, int maxCount) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; int i; if (maxCount > kMaxBlocksPerIndex) maxCount = kMaxBlocksPerIndex; dierr = fpDiskFS->GetDiskImg()->ReadBlock(block, blkBuf); if (dierr != kDIErrNone) goto bail; //LOGI("LOADING 0x%04x", block); for (i = 0; i < maxCount; i++) { *list++ = blkBuf[i] | (uint16_t) blkBuf[i+256] << 8; } bail: return dierr; } /* * Load the block list from a directory, which is essentially a linear * linked list. */ DIError A2FileProDOS::LoadDirectoryBlockList(uint16_t keyBlock, long eof, long* pBlockCount, uint16_t** pBlockList) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; uint16_t* list = NULL; uint16_t* listPtr; int iterations; long count; assert(eof < 1024*1024*16); count = (eof + kBlkSize -1) / kBlkSize; if (count == 0) count = 1; list = new uint16_t[count+1]; if (list == NULL) { dierr = kDIErrMalloc; goto bail; } /* this should take care of trailing sparse entries */ memset(list, 0, sizeof(uint16_t) * count); list[count] = kInvalidBlockNum; // overrun check iterations = 0; listPtr = list; while (keyBlock && iterations < kMaxCatalogIterations) { if (keyBlock < 2 || keyBlock >= fpDiskFS->GetDiskImg()->GetNumBlocks()) { LOGI(" ProDOS ERROR: directory block %u out of range", keyBlock); dierr = kDIErrInvalidBlock; goto bail; } *listPtr++ = keyBlock; dierr = fpDiskFS->GetDiskImg()->ReadBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; keyBlock = GetShortLE(&blkBuf[0x02]); iterations++; } if (iterations == kMaxCatalogIterations) { LOGI(" ProDOS subdir iteration count exceeded"); dierr = kDIErrDirectoryLoop; goto bail; } assert(list[count] == kInvalidBlockNum); *pBlockCount = count; *pBlockList = list; bail: if (dierr != kDIErrNone) delete[] list; return dierr; } /* * Dump the contents. */ void A2FileProDOS::Dump(void) const { LOGI(" ProDOS file '%s' (path='%s')", fDirEntry.fileName, fPathName); LOGI(" fileType=0x%02x auxType=0x%04x storage=%d", fDirEntry.fileType, fDirEntry.auxType, fDirEntry.storageType); LOGI(" keyPointer=%d blocksUsed=%d eof=%d", fDirEntry.keyPointer, fDirEntry.blocksUsed, fDirEntry.eof); LOGI(" access=0x%02x create=0x%08x mod=0x%08x", fDirEntry.access, fDirEntry.createWhen, fDirEntry.modWhen); LOGI(" version=%d minVersion=%d headerPtr=%d", fDirEntry.version, fDirEntry.minVersion, fDirEntry.headerPointer); if (fDirEntry.storageType == kStorageExtended) { LOGI(" DATA storage=%d keyBlk=%d blkUsed=%d eof=%d", fExtData.storageType, fExtData.keyBlock, fExtData.blocksUsed, fExtData.eof); LOGI(" RSRC storage=%d keyBlk=%d blkUsed=%d eof=%d", fExtRsrc.storageType, fExtRsrc.keyBlock, fExtRsrc.blocksUsed, fExtRsrc.eof); } LOGI(" * sparseData=%ld sparseRsrc=%ld", (long) fSparseDataEof, (long) fSparseRsrcEof); } /* * =========================================================================== * A2FDProDOS * =========================================================================== */ /* * Read a chunk of data from whichever fork is open. */ DIError A2FDProDOS::Read(void* buf, size_t len, size_t* pActual) { LOGD(" ProDOS reading %lu bytes from '%s' (offset=%ld)", (unsigned long) len, fpFile->GetPathName(), (long) fOffset); //if (fBlockList == NULL) // return kDIErrNotReady; if (fOffset + (long)len > fOpenEOF) { if (pActual == NULL) return kDIErrDataUnderrun; len = (long) (fOpenEOF - fOffset); } if (pActual != NULL) *pActual = len; // long incrLen = len; DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; long blockIndex = (long) (fOffset / kBlkSize); int bufOffset = (int) (fOffset % kBlkSize); // (& 0x01ff) size_t thisCount; long progressCounter = 0; if (len == 0) { ///* one block allocated for empty file */ //SetLastBlock(fBlockList[0], true); return kDIErrNone; } assert(fOpenEOF != 0); assert(blockIndex >= 0 && blockIndex < fBlockCount); while (len) { if (fBlockList[blockIndex] == 0) { //LOGI(" ProDOS sparse index %d", blockIndex); memset(blkBuf, 0, sizeof(blkBuf)); } else { //LOGI(" ProDOS non-sparse index %d", blockIndex); dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock(fBlockList[blockIndex], blkBuf); if (dierr != kDIErrNone) { LOGI(" ProDOS error reading block [%ld]=%d of '%s'", blockIndex, fBlockList[blockIndex], fpFile->GetPathName()); return dierr; } } thisCount = kBlkSize - bufOffset; if (thisCount > len) thisCount = len; memcpy(buf, blkBuf + bufOffset, thisCount); len -= thisCount; buf = (char*)buf + thisCount; bufOffset = 0; blockIndex++; progressCounter++; if (progressCounter > 100 && len) { progressCounter = 0; /* * Show progress within the current read request. This only * kicks in for large reads, e.g. reformatting the entire file. * For smaller reads, used when we're extracting w/o reformatting, * "progressCounter" never gets large enough. */ if (!UpdateProgress(fOffset + incrLen - len)) { dierr = kDIErrCancelled; return dierr; } //::Sleep(100); // DEBUG DEBUG } } fOffset += incrLen; if (!UpdateProgress(fOffset)) dierr = kDIErrCancelled; return dierr; } /* * Write data at the current offset. * * For simplicity, we assume that there can only be one of two situations: * (1) We're writing a directory, which might expand by one block; or * (2) We're writing all of a brand-new file in one shot. * * Modifies fOpenEOF, fOpenBlocksUsed, fStorageType, and sets fModified. * * HEY: ProSel-16 describes these as fragmented, and it's probably right. * The correct way to do this is to allocate index blocks before allocating * the blocks they refer to, so that we don't have to jump all over the disk * to read the indexes (which, at the moment, appear at the end of the file). * A bit tricky, but doable. */ DIError A2FDProDOS::Write(const void* buf, size_t len, size_t* pActual) { DIError dierr = kDIErrNone; A2FileProDOS* pFile = (A2FileProDOS*) fpFile; DiskFSProDOS* pDiskFS = (DiskFSProDOS*) fpFile->GetDiskFS(); bool allocSparse = (pDiskFS->GetParameter(DiskFS::kParmProDOS_AllocSparse) != 0); uint8_t blkBuf[kBlkSize]; uint16_t keyBlock; bool allZero = true; const uint8_t* scanPtr = (const uint8_t*)buf; if (len >= 0x01000000) { // 16MB assert(false); return kDIErrInvalidArg; } /* use separate function for directories */ if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageDirectory || pFile->fDirEntry.storageType == A2FileProDOS::kStorageVolumeDirHeader) { return WriteDirectory(buf, len, pActual); } dierr = pDiskFS->LoadVolBitmap(); if (dierr != kDIErrNone) goto bail; assert(fOffset == 0); // big simplifying assumption assert(fOpenEOF == 0); // another one assert(fOpenBlocksUsed == 1); assert(buf != NULL); /* nothing to do for zero-length write; don't even set fModified */ if (len == 0) goto bail; if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended) keyBlock = pFile->fDirEntry.keyPointer; else { if (fOpenRsrcFork) keyBlock = pFile->fExtRsrc.keyBlock; else keyBlock = pFile->fExtData.keyBlock; } /* * See if the file is completely empty. This lets us do an optimization * where we store it as a seedling. (GS/OS seems to do this, ProDOS 8 * v2.0.3 tends to allocate the first block.) */ for (unsigned int i = 0; i < len; ++i, ++scanPtr) { if (*scanPtr != 0x00) { allZero = false; break; } } if (allZero) { LOGI("+++ found file filled with %zd zeroes", len); } /* * Special-case seedling files. Just write the data into the key block * and we're done. */ if (allZero || len <= (size_t)kBlkSize) { memset(blkBuf, 0, sizeof(blkBuf)); if (!allZero) { memcpy(blkBuf, buf, len); } else { LOGI("+++ ProDOS storing large but empty file as seedling"); } dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; fOpenEOF = len; fOpenBlocksUsed = 1; assert(fOpenStorageType == A2FileProDOS::kStorageSeedling); fOffset += len; fModified = true; goto bail; } /* * Start by allocating space for the block list. The list is always the * same size, regardless of sparse allocations. * * We over-alloc by one so we can have an overrun detection entry. */ fBlockCount = (len + kBlkSize-1) / kBlkSize; assert(fBlockCount > 0); delete[] fBlockList; fBlockList = new uint16_t[fBlockCount+1]; if (fBlockList == NULL) { dierr = kDIErrMalloc; goto bail; } fBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum; /* * Write the data blocks to disk, allocating as we go. We have to treat * the last entry specially because it might not fill an entire block. */ const uint8_t* blkPtr; long blockIdx; long progressCounter; progressCounter = 0; blkPtr = (const uint8_t*) buf; for (blockIdx = 0; blockIdx < fBlockCount; blockIdx++) { long newBlock; if (blockIdx == fBlockCount-1) { /* for last block, copy partial and move blkPtr */ int copyLen = len - (blockIdx * kBlkSize); assert(copyLen > 0 && copyLen <= kBlkSize); memset(blkBuf, 0, sizeof(blkBuf)); memcpy(blkBuf, blkPtr, copyLen); blkPtr = blkBuf; } if (allocSparse && IsEmptyBlock(blkPtr)) { if (blockIdx == 0) { // Fix for issues #18 and #49. GS/OS appears to get confused // if the first entry in the master index block for a "tree" // file is zero. We can avoid the problem by always allocating // the first data block, which causes allocation of the first // index block. (The "all zeroes" case was handled earlier, // so if we got here we know this won't be an empty seedling.) LOGI("+++ allocating storage for empty first block"); newBlock = pDiskFS->AllocBlock(); fOpenBlocksUsed++; } else { // Sparse. newBlock = 0; } } else { newBlock = pDiskFS->AllocBlock(); fOpenBlocksUsed++; } if (newBlock < 0) { LOGI(" ProDOS disk full during write!"); dierr = kDIErrDiskFull; goto bail; } fBlockList[blockIdx] = (uint16_t) newBlock; if (newBlock != 0) { dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkPtr); if (dierr != kDIErrNone) goto bail; } blkPtr += kBlkSize; /* * Update the progress counter and check to see if the "cancel" button * has been hit. We don't call UpdateProgress on the last block * because we could be passing an offset value larger than "len". * Also, we don't want the progress bar to hit 100% until we've * actually finished. * * We do NOT want to check this after we start writing index blocks. * If we do, we need to make sure that whatever index blocks the file * has match up with what we've allocated in the disk block map. * * We don't want to save the disk block map if the user cancels here, * because then the blocks will be marked as "used" even though the * index blocks for this file haven't been written yet. * * It's tricky to get this right, which is why we allocate space * for the index blocks now -- running out of disk space and * user cancellation are handled the same way. Once we get to the * point where we're updating the file structure, we can neither be * cancelled nor run out of space. (We can still hit a bad block, * though, which we currently don't handle.) */ progressCounter++; // update every N blocks if (progressCounter > 100 && blockIdx != fBlockCount) { progressCounter = 0; if (!UpdateProgress(blockIdx * kBlkSize)) { dierr = kDIErrCancelled; goto bail; } } } assert(fBlockList[fBlockCount] == A2FileProDOS::kInvalidBlockNum); /* * Now we have a full block map. Allocate any needed index blocks and * write them. */ #if 0 // now done earlier /* * If our block map is empty, i.e. the entire file is sparse, then * there's no need to create a sapling. We just leave the file in * seedling form. This can only happen for a completely empty file. */ if (allZero) { LOGI("+++ ProDOS storing large but empty file as seedling"); /* make sure key block is empty */ memset(blkBuf, 0, sizeof(blkBuf)); dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; fOpenStorageType = A2FileProDOS::kStorageSeedling; fBlockList[0] = keyBlock; } else #endif if (fBlockCount <= 256) { /* sapling file, write an index block into the key block */ //bool allzero = true; <-- should this be getting used? assert(fBlockCount > 1); memset(blkBuf, 0, sizeof(blkBuf)); int i; for (i = 0; i < fBlockCount; i++) { //if (fBlockList[i] != 0) // allzero = false; blkBuf[i] = fBlockList[i] & 0xff; blkBuf[256 + i] = (fBlockList[i] >> 8) & 0xff; } dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, blkBuf); if (dierr != kDIErrNone) goto bail; fOpenStorageType = A2FileProDOS::kStorageSapling; } else { /* tree file, write two or more indexes and write master into key */ uint8_t masterBlk[kBlkSize]; int idx; memset(masterBlk, 0, sizeof(masterBlk)); for (idx = 0; idx < fBlockCount; ) { long newBlock; int i; memset(blkBuf, 0, sizeof(blkBuf)); for (i = 0; i < 256 && idx < fBlockCount; i++, idx++) { blkBuf[i] = fBlockList[idx] & 0xff; blkBuf[256+i] = (fBlockList[idx] >> 8) & 0xff; } /* allocate a new index block, if needed */ if (allocSparse && IsEmptyBlock(blkBuf)) newBlock = 0; else { newBlock = pDiskFS->AllocBlock(); fOpenBlocksUsed++; } if (newBlock != 0) { dierr = pDiskFS->GetDiskImg()->WriteBlock(newBlock, blkBuf); if (dierr != kDIErrNone) goto bail; } masterBlk[(idx-1) / 256] = (uint8_t) newBlock; masterBlk[256 + (idx-1)/256] = (uint8_t) (newBlock >> 8); } dierr = pDiskFS->GetDiskImg()->WriteBlock(keyBlock, masterBlk); if (dierr != kDIErrNone) goto bail; fOpenStorageType = A2FileProDOS::kStorageTree; } fOpenEOF = len; fOffset += len; fModified = true; bail: if (dierr == kDIErrNone) dierr = pDiskFS->SaveVolBitmap(); /* * We need to check UpdateProgress *after* the volume bitmap has been * saved. Otherwise we'll have blocks allocated in the file's structure * but not marked in-use in the map when the "dierr" check above fails. */ if (dierr == kDIErrNone) { if (!UpdateProgress(fOffset)) dierr = kDIErrCancelled; } pDiskFS->FreeVolBitmap(); return dierr; } /* * Determine whether a block is filled entirely with zeroes. */ bool A2FDProDOS::IsEmptyBlock(const uint8_t* blk) { int i; for (i = 0; i < kBlkSize; i++) { if (*blk++ != 0) return false; } return true; } /* * Write a directory, possibly extending it by one block. * * If we're growing, the extra block will already have been allocated, and is * pointed to by the "next" pointer in the next-to-last block. (This * pre-allocation makes our lives easier, and avoids a situation where we * would have to update the volume bitmap when another function is already * making lots of changes to it.) */ DIError A2FDProDOS::WriteDirectory(const void* buf, size_t len, size_t* pActual) { DIError dierr = kDIErrNone; LOGD("ProDOS writing %lu bytes to directory '%s'", (unsigned long) len, fpFile->GetPathName()); assert(len >= (size_t)kBlkSize); assert((len % kBlkSize) == 0); assert(len == (size_t)fOpenEOF || len == (size_t)fOpenEOF + kBlkSize); if (len > (size_t)fOpenEOF) { /* * Extend the block list, remembering that we add an extra item * on the end to check for overruns. */ uint16_t* newBlockList; fBlockCount++; newBlockList = new uint16_t[fBlockCount+1]; memcpy(newBlockList, fBlockList, sizeof(uint16_t) * fBlockCount); newBlockList[fBlockCount] = A2FileProDOS::kInvalidBlockNum; uint8_t* blkPtr; blkPtr = (uint8_t*)buf + fOpenEOF - kBlkSize; assert(blkPtr >= buf); assert(GetShortLE(&blkPtr[0x02]) != 0); newBlockList[fBlockCount-1] = GetShortLE(&blkPtr[0x02]); delete[] fBlockList; fBlockList = newBlockList; LOGI(" ProDOS updated block list for subdir:"); DumpBlockList(); } /* * Now just run down the block list writing the directory. */ assert(len == (size_t)fBlockCount * kBlkSize); int idx; for (idx = 0; idx < fBlockCount; idx++) { assert(fBlockList[idx] >= kVolHeaderBlock); dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock(fBlockList[idx], (uint8_t*)buf + idx * kBlkSize); if (dierr != kDIErrNone) { LOGI(" ProDOS failed writing dir, block=%d", fBlockList[idx]); goto bail; } } fOpenEOF = len; fOpenBlocksUsed = (uint16_t) fBlockCount; // very simple for subdirs //fOpenStorageType fModified = true; bail: return dierr; } /* * Seek to a new position within the file. */ DIError A2FDProDOS::Seek(di_off_t offset, DIWhence whence) { DIError dierr = kDIErrNone; switch (whence) { case kSeekSet: if (offset < 0 || offset > fOpenEOF) return kDIErrInvalidArg; fOffset = offset; break; case kSeekEnd: if (offset > 0 || offset < -fOpenEOF) return kDIErrInvalidArg; fOffset = fOpenEOF + offset; break; case kSeekCur: if (offset < -fOffset || offset >= (fOpenEOF - fOffset)) { return kDIErrInvalidArg; } fOffset += offset; break; default: assert(false); return kDIErrInvalidArg; } assert(fOffset >= 0 && fOffset <= fOpenEOF); return dierr; } /* * Return current offset. */ di_off_t A2FDProDOS::Tell(void) { //if (fBlockList == NULL) // return kDIErrNotReady; return fOffset; } /* * Release file state. * * Most applications don't check the value of "Close", or call it from a * destructor, so we call CloseDescr whether we succeed or not. */ DIError A2FDProDOS::Close(void) { DIError dierr = kDIErrNone; if (fModified) { A2FileProDOS* pFile = (A2FileProDOS*) fpFile; uint8_t blkBuf[kBlkSize]; uint8_t newStorageType = fOpenStorageType; uint16_t newBlocksUsed = fOpenBlocksUsed; uint32_t newEOF = (uint32_t) fOpenEOF; // TODO: assert range uint16_t combinedBlocksUsed; uint32_t combinedEOF; /* * If this is an extended file, fix the entries in the extended * key block, and adjust the values to be stored in the directory. */ if (pFile->fDirEntry.storageType == A2FileProDOS::kStorageExtended) { /* these two don't change */ newStorageType = pFile->fDirEntry.storageType; dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock( pFile->fDirEntry.keyPointer, blkBuf); if (dierr != kDIErrNone) goto bail; int offset = 0; if (fOpenRsrcFork) offset = 256; blkBuf[0x00 + offset] = fOpenStorageType; // key block doesn't change PutShortLE(&blkBuf[0x03 + offset], newBlocksUsed); blkBuf[0x05 + offset] = (uint8_t) newEOF; blkBuf[0x06 + offset] = (uint8_t) (newEOF >> 8); blkBuf[0x07 + offset] = (uint8_t) (newEOF >> 16); dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock( pFile->fDirEntry.keyPointer, blkBuf); if (dierr != kDIErrNone) goto bail; // file blocks used is sum of data and rsrc block counts +1 for key combinedBlocksUsed = GetShortLE(&blkBuf[0x03]) + GetShortLE(&blkBuf[0x103]) +1; combinedEOF = 512; // for some reason this gets stuffed in } else { combinedBlocksUsed = newBlocksUsed; combinedEOF = newEOF; } /* * Update fields in the file's directory entry. Unless, of course, * this is the volume directory itself. */ if (pFile->fParentDirBlock != 0) { dierr = fpFile->GetDiskFS()->GetDiskImg()->ReadBlock( pFile->fParentDirBlock, blkBuf); if (dierr != kDIErrNone) goto bail; uint8_t* pParentPtr; pParentPtr = blkBuf + 0x04 + pFile->fParentDirIdx * kEntryLength; assert(pParentPtr + kEntryLength < blkBuf + kBlkSize); if (toupper(pParentPtr[0x01]) != toupper(pFile->fDirEntry.fileName[0])) { LOGW("ProDOS ERROR: parent pointer has wrong entry??"); assert(false); dierr = kDIErrInternal; goto bail; } /* update the fields from the open file */ pParentPtr[0x00] = (pParentPtr[0x00] & 0x0f) | (newStorageType << 4); PutShortLE(&pParentPtr[0x13], combinedBlocksUsed); if (pFile->fDirEntry.storageType != A2FileProDOS::kStorageExtended) { PutShortLE(&pParentPtr[0x15], (uint16_t) newEOF); pParentPtr[0x17] = (uint8_t) (newEOF >> 16); } /* don't update the mod date for now */ //PutLongLE(&pParentPtr[0x21], A2FileProDOS::ConvertProDate(time(NULL))); dierr = fpFile->GetDiskFS()->GetDiskImg()->WriteBlock( pFile->fParentDirBlock, blkBuf); if (dierr != kDIErrNone) goto bail; } /* * Find the #of sparse blocks. We do this to update the "sparse EOF", * which determines the "compressed" size shown in the file list. We * have two cases: normal file with sparse contents, and seedling file * that is entirely sparse except for the first block. * * In the normal case, we walk through the list of data blocks, * looking for gaps. In the seedling case, we just use the EOF. * * This is just for display. The value seen after adding a file should * not change if you reload the disk image. */ int sparseBlocks = 0; if (fBlockCount == 1 && fOpenEOF > kBlkSize) { // 1023/1024 = 2 blocks = 1 sparse // 1025 = 3 blocks = 2 sparse sparseBlocks = (int)((fOpenEOF-1) / kBlkSize); } else { for (int i = 0; i < fBlockCount; i++) { if (fBlockList[i] == 0) sparseBlocks++; } } /* * Update our internal copies of stuff. The EOFs have changed, and * in theory we'd want to update the modification date. In practice * we're usually shuffling data from one archive to another and want * to preserve the mod date. (Could be a DiskFS global pref?) */ pFile->fDirEntry.storageType = newStorageType; pFile->fDirEntry.blocksUsed = combinedBlocksUsed; pFile->fDirEntry.eof = combinedEOF; if (newStorageType == A2FileProDOS::kStorageExtended) { if (!fOpenRsrcFork) { pFile->fExtData.storageType = fOpenStorageType; pFile->fExtData.blocksUsed = newBlocksUsed; pFile->fExtData.eof = newEOF; pFile->fSparseDataEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize); if (pFile->fSparseDataEof < 0) pFile->fSparseDataEof = 0; } else { pFile->fExtRsrc.storageType = fOpenStorageType; pFile->fExtRsrc.blocksUsed = newBlocksUsed; pFile->fExtRsrc.eof = newEOF; pFile->fSparseRsrcEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize); if (pFile->fSparseRsrcEof < 0) pFile->fSparseRsrcEof = 0; } } else { pFile->fSparseDataEof = (di_off_t) newEOF - (sparseBlocks * kBlkSize); if (pFile->fSparseDataEof < 0) pFile->fSparseDataEof = 0; } // update mod date? //LOGI("File '%s' closed", pFile->GetPathName()); //pFile->Dump(); } bail: fpFile->CloseDescr(this); return dierr; } /* * Return the #of sectors/blocks in the file. */ long A2FDProDOS::GetSectorCount(void) const { //if (fBlockList == NULL) // return kDIErrNotReady; return fBlockCount * 2; } long A2FDProDOS::GetBlockCount(void) const { //if (fBlockList == NULL) // return kDIErrNotReady; return fBlockCount; } /* * Return the Nth track/sector in this file. */ DIError A2FDProDOS::GetStorage(long sectorIdx, long* pTrack, long* pSector) const { //if (fBlockList == NULL) // return kDIErrNotReady; long prodosIdx = sectorIdx / 2; if (prodosIdx < 0 || prodosIdx >= fBlockCount) return kDIErrInvalidIndex; long prodosBlock = fBlockList[prodosIdx]; if (prodosBlock == 0) *pTrack = *pSector = 0; // special-case to avoid returning (0,1) else BlockToTrackSector(prodosBlock, (sectorIdx & 0x01) != 0, pTrack, pSector); return kDIErrNone; } /* * Return the Nth 512-byte block in this file. */ DIError A2FDProDOS::GetStorage(long blockIdx, long* pBlock) const { //if (fBlockList == NULL) // return kDIErrNotReady; if (blockIdx < 0 || blockIdx >= fBlockCount) return kDIErrInvalidIndex; long prodosBlock = fBlockList[blockIdx]; *pBlock = prodosBlock; assert(*pBlock < fpFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); return kDIErrNone; } /* * Dump the list of blocks from an open file, skipping over * "sparsed-out" entries. */ void A2FDProDOS::DumpBlockList(void) const { long ll; LOGI(" ProDOS file block list (count=%ld)", fBlockCount); for (ll = 0; ll <= fBlockCount; ll++) { if (fBlockList[ll] != 0) { LOGI(" %5ld: 0x%04x", ll, fBlockList[ll]); } } }