/* * CiderPress * Copyright (C) 2007 by faddenSoft, LLC. All Rights Reserved. * See the file LICENSE for distribution terms. */ /* * Implementation of DiskFSPascal class. * * Currently each file may only be open once. */ #include "StdAfx.h" #include "DiskImgPriv.h" /* * =========================================================================== * DiskFSPascal * =========================================================================== */ const int kBlkSize = 512; const int kVolHeaderBlock = 2; // first directory block const int kMaxCatalogIterations = 64; // should be short, linear catalog const int kHugeDir = 32; static const char* kInvalidNameChars = "$=?,[#:"; /* * See if this looks like a Pascal volume. * * We test a few fields in the volume directory for validity. */ static DIError TestImage(DiskImg* pImg, DiskImg::SectorOrder imageOrder) { DIError dierr = kDIErrNone; uint8_t blkBuf[512]; uint8_t volName[DiskFSPascal::kMaxVolumeName+1]; dierr = pImg->ReadBlockSwapped(kVolHeaderBlock, blkBuf, imageOrder, DiskImg::kSectorOrderProDOS); if (dierr != kDIErrNone) goto bail; if (!(blkBuf[0x00] == 0 && blkBuf[0x01] == 0) || !(blkBuf[0x04] == 0 && blkBuf[0x05] == 0) || !(blkBuf[0x06] > 0 && blkBuf[0x06] <= DiskFSPascal::kMaxVolumeName) || 0) { dierr = kDIErrFilesystemNotFound; goto bail; } /* volume name length is good, check the name itself */ /* (this may be overly restrictive, but it's probably good to be) */ memset(volName, 0, sizeof(volName)); memcpy(volName, &blkBuf[0x07], blkBuf[0x06]); if (!DiskFSPascal::IsValidVolumeName((const char*) volName)) return kDIErrFilesystemNotFound; bail: return dierr; } /* * Test to see if the image is a Pascal disk. */ /*static*/ DIError DiskFSPascal::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::kFormatPascal; return kDIErrNone; } } LOGI(" Pascal 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 is * completely full and has no files on it. */ DIError DiskFSPascal::Initialize(void) { DIError dierr = kDIErrNone; fDiskIsGood = false; // hosed until proven innocent fEarlyDamage = false; fVolumeUsage.Create(fpImg->GetNumBlocks()); dierr = LoadVolHeader(); if (dierr != kDIErrNone) goto bail; DumpVolHeader(); dierr = ProcessCatalog(); if (dierr != kDIErrNone) goto bail; dierr = ScanFileUsage(); if (dierr != kDIErrNone) { /* this might not be fatal; just means that *some* files are bad */ goto bail; } fDiskIsGood = CheckDiskIsGood(); 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. */ DIError DiskFSPascal::LoadVolHeader(void) { DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; int nameLen, maxFiles; dierr = fpImg->ReadBlock(kVolHeaderBlock, blkBuf); if (dierr != kDIErrNone) goto bail; /* vol header is same size as dir entry, but different layout */ fStartBlock = GetShortLE(&blkBuf[0x00]); assert(fStartBlock == 0); // verified in "TestImage" fNextBlock = GetShortLE(&blkBuf[0x02]); assert(GetShortLE(&blkBuf[0x04]) == 0); // type nameLen = blkBuf[0x06] & 0x07; memcpy(fVolumeName, &blkBuf[0x07], nameLen); fVolumeName[nameLen] = '\0'; fTotalBlocks = GetShortLE(&blkBuf[0x0e]); fNumFiles = GetShortLE(&blkBuf[0x10]); fAccessWhen = GetShortLE(&blkBuf[0x12]); // time of last access fDateSetWhen = GetShortLE(&blkBuf[0x14]); // most recent date set fStuff1 = GetShortLE(&blkBuf[0x16]); // filler fStuff2 = GetShortLE(&blkBuf[0x18]); // filler if (fTotalBlocks != fpImg->GetNumBlocks()) { // saw this most recently on a 40-track .APP image; not a problem LOGI(" Pascal WARNING: total (%u) != img (%ld)", fTotalBlocks, fpImg->GetNumBlocks()); } /* * Sanity checks. */ if (fNextBlock > 34) { // directory really shouldn't be more than 6; I'm being generous fpImg->AddNote(DiskImg::kNoteWarning, "Pascal directory is too big (%d blocks); trimming.", fNextBlock - fStartBlock); } /* max #of file entries, including the vol dir header */ maxFiles = ((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen; if (fNumFiles > maxFiles-1) { fpImg->AddNote(DiskImg::kNoteWarning, "Pascal fNumFiles (%d) exceeds max files (%d); trimming.\n", fNumFiles, maxFiles-1); fEarlyDamage = true; } SetVolumeID(); bail: return dierr; } /* * Set the volume ID field. */ void DiskFSPascal::SetVolumeID(void) { sprintf(fVolumeID, "Pascal %s:", fVolumeName); } /* * Dump what we pulled out of the volume header. */ void DiskFSPascal::DumpVolHeader(void) { time_t access, dateSet; LOGI(" Pascal volume header for '%s'", fVolumeName); LOGI(" startBlock=%d nextBlock=%d", fStartBlock, fNextBlock); LOGI(" totalBlocks=%d numFiles=%d access=0x%04x dateSet=0x%04x", fTotalBlocks, fNumFiles, fAccessWhen, fDateSetWhen); access = A2FilePascal::ConvertPascalDate(fAccessWhen); dateSet = A2FilePascal::ConvertPascalDate(fDateSetWhen); LOGI(" -->access %.24s", ctime(&access)); LOGI(" -->dateSet %.24s", ctime(&dateSet)); //LOGI("Unconvert access=0x%04x dateSet=0x%04x", // A2FilePascal::ConvertPascalDate(access), // A2FilePascal::ConvertPascalDate(dateSet)); } /* * Read the catalog from the disk. * * No distinction is made for block boundaries, so we want to slurp the * entire thing into memory. * * Sets "fDirectory". */ DIError DiskFSPascal::LoadCatalog(void) { DIError dierr = kDIErrNone; uint8_t* dirPtr; int block, numBlocks; assert(fDirectory == NULL); numBlocks = fNextBlock - kVolHeaderBlock; if (numBlocks <= 0 || numBlocks > kHugeDir) { dierr = kDIErrBadDiskImage; goto bail; } fDirectory = new uint8_t[kBlkSize * numBlocks]; if (fDirectory == NULL) { dierr = kDIErrMalloc; goto bail; } block = kVolHeaderBlock; dirPtr = fDirectory; while (numBlocks--) { dierr = fpImg->ReadBlock(block, dirPtr); if (dierr != kDIErrNone) goto bail; block++; dirPtr += kBlkSize; } bail: if (dierr != kDIErrNone) { delete[] fDirectory; fDirectory = NULL; } return dierr; } /* * Write our copy of the catalog back out to disk. */ DIError DiskFSPascal::SaveCatalog(void) { DIError dierr = kDIErrNone; uint8_t* dirPtr; int block, numBlocks; assert(fDirectory != NULL); numBlocks = fNextBlock - kVolHeaderBlock; block = kVolHeaderBlock; dirPtr = fDirectory; while (numBlocks--) { dierr = fpImg->WriteBlock(block, dirPtr); if (dierr != kDIErrNone) goto bail; block++; dirPtr += kBlkSize; } bail: return dierr; } /* * Free the catalog storage. */ void DiskFSPascal::FreeCatalog(void) { delete[] fDirectory; fDirectory = NULL; } /* * Process the catalog into A2File structures. */ DIError DiskFSPascal::ProcessCatalog(void) { DIError dierr = kDIErrNone; int i, nameLen; A2FilePascal* pFile; const uint8_t* dirPtr; uint16_t prevNextBlock = fNextBlock; dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; dirPtr = fDirectory + kDirectoryEntryLen; // skip vol dir entry for (i = 0; i < fNumFiles; i++) { pFile = new A2FilePascal(this); pFile->fStartBlock = GetShortLE(&dirPtr[0x00]); pFile->fNextBlock = GetShortLE(&dirPtr[0x02]); pFile->fFileType = (A2FilePascal::FileType) GetShortLE(&dirPtr[0x04]); nameLen = dirPtr[0x06] & 0x0f; memcpy(pFile->fFileName, &dirPtr[0x07], nameLen); pFile->fFileName[nameLen] = '\0'; pFile->fBytesRemaining = GetShortLE(&dirPtr[0x16]); pFile->fModWhen = GetShortLE(&dirPtr[0x18]); /* check bytesRem before setting length field */ if (pFile->fBytesRemaining > kBlkSize) { LOGI(" Pascal found strange bytesRem %u on '%s', trimming", pFile->fBytesRemaining, pFile->fFileName); pFile->fBytesRemaining = kBlkSize; pFile->SetQuality(A2File::kQualitySuspicious); } pFile->fLength = pFile->fBytesRemaining + (pFile->fNextBlock - pFile->fStartBlock -1) * kBlkSize; /* * Check values. */ if (pFile->fStartBlock == pFile->fNextBlock) { LOGI(" Pascal found zero-block file '%s'", pFile->fFileName); pFile->SetQuality(A2File::kQualityDamaged); } if (pFile->fStartBlock < prevNextBlock) { LOGI(" Pascal start of '%s' (%d) overlaps previous end (%d)", pFile->fFileName, pFile->fStartBlock, prevNextBlock); pFile->SetQuality(A2File::kQualityDamaged); } if (pFile->fNextBlock > fpImg->GetNumBlocks()) { LOGI(" Pascal invalid 'next' block %d (max %ld) '%s'", pFile->fNextBlock, fpImg->GetNumBlocks(), pFile->fFileName); pFile->fStartBlock = pFile->fNextBlock = 0; pFile->fLength = 0; pFile->SetQuality(A2File::kQualityDamaged); } else if (pFile->fNextBlock > fTotalBlocks) { LOGI(" Pascal 'next' block %d exceeds max (%d) '%s'", pFile->fNextBlock, fTotalBlocks, pFile->fFileName); pFile->SetQuality(A2File::kQualitySuspicious); } //pFile->Dump(); AddFileToList(pFile); dirPtr += kDirectoryEntryLen; prevNextBlock = pFile->fNextBlock; } bail: FreeCatalog(); return dierr; } /* * Create the volume usage map. Since UCSD Pascal volumes have neither * in-use maps nor index blocks, this is pretty straightforward. */ DIError DiskFSPascal::ScanFileUsage(void) { int block; /* start with the boot blocks */ SetBlockUsage(0, VolumeUsage::kChunkPurposeSystem); SetBlockUsage(1, VolumeUsage::kChunkPurposeSystem); for (block = kVolHeaderBlock; block < fNextBlock; block++) { SetBlockUsage(block, VolumeUsage::kChunkPurposeVolumeDir); } A2FilePascal* pFile; pFile = (A2FilePascal*) GetNextFile(NULL); while (pFile != NULL) { for (block = pFile->fStartBlock; block < pFile->fNextBlock; block++) SetBlockUsage(block, VolumeUsage::kChunkPurposeUserData); pFile = (A2FilePascal*) GetNextFile(pFile); } return kDIErrNone; } /* * Update an entry in the volume usage map. */ void DiskFSPascal::SetBlockUsage(long block, VolumeUsage::ChunkPurpose purpose) { VolumeUsage::ChunkState cstate; fVolumeUsage.GetChunkState(block, &cstate); if (cstate.isUsed) { cstate.purpose = VolumeUsage::kChunkPurposeConflict; LOGI(" Pascal conflicting uses for bl=%ld", block); } else { cstate.isUsed = true; cstate.isMarkedUsed = true; cstate.purpose = purpose; } fVolumeUsage.SetChunkState(block, &cstate); } /* * Test a string for validity as a Pascal volume name. * * Volume names can only be 7 characters long, but otherwise obey the same * rules as file names. */ /*static*/ bool DiskFSPascal::IsValidVolumeName(const char* name) { if (name == NULL) { assert(false); return false; } if (strlen(name) > kMaxVolumeName) return false; return IsValidFileName(name); } /* * Test a string for validity as a Pascal file name. * * Filenames can be up to 15 characters long. All characters are valid. * However, the system filer gets bent out of shape if you use spaces, * control characters, any of the wildcards ($=?), or filer meta-characters * (,[#:). It also converts all alpha characters to upper case, but we can * take care of that later. */ /*static*/ bool DiskFSPascal::IsValidFileName(const char* name) { assert(name != NULL); if (name[0] == '\0') return false; if (strlen(name) > A2FilePascal::kMaxFileName) return false; /* must be A-Z 0-9 '.' */ while (*name != '\0') { if (*name <= 0x20 || *name >= 0x7f) // no space, del, or ctrl return false; //if (*name >= 'a' && *name <= 'z') // no lower case // return false; if (strchr(kInvalidNameChars, *name) != NULL) // filer metacharacters return false; name++; } return true; } /* * Put a Pascal filesystem image on the specified DiskImg. */ DIError DiskFSPascal::Format(DiskImg* pDiskImg, const char* volName) { DIError dierr = kDIErrNone; 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(" Pascal formatting disk image"); /* write ProDOS-style blocks */ dierr = fpImg->OverrideFormat(fpImg->GetPhysicalFormat(), DiskImg::kFormatGenericProDOSOrd, fpImg->GetSectorOrder()); if (dierr != kDIErrNone) goto bail; formatBlocks = pDiskImg->GetNumBlocks(); if (formatBlocks != 280 && formatBlocks != 1600) { LOGI(" Pascal: rejecting format req blocks=%ld", formatBlocks); assert(false); return kDIErrInvalidArg; } /* * We should now zero out the disk blocks, but this is done automatically * on new disk images, so there's no need to do it here. */ // dierr = fpImg->ZeroImage(); LOGI(" Pascal (not zeroing blocks)"); /* * Start by writing blocks 0 and 1 (the boot blocks). The file * APPLE3:FORMATTER.DATA holds images for 3.5" and 5.25" disks. */ dierr = WriteBootBlocks(); if (dierr != kDIErrNone) goto bail; /* * Write the disk volume entry. */ memset(blkBuf, 0, sizeof(blkBuf)); PutShortLE(&blkBuf[0x00], 0); // start block PutShortLE(&blkBuf[0x02], 6); // next block PutShortLE(&blkBuf[0x04], 0); // "file" type blkBuf[0x06] = (uint8_t)strlen(volName); memcpy(&blkBuf[0x07], volName, strlen(volName)); PutShortLE(&blkBuf[0x0e], (uint16_t) pDiskImg->GetNumBlocks()); PutShortLE(&blkBuf[0x10], 0); // num files PutShortLE(&blkBuf[0x12], 0); // last access date PutShortLE(&blkBuf[0x14], 0xa87b); // last date set (Nov 7 1984) 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; } bail: SetDiskImg(NULL); // shouldn't really be set by us return dierr; } /* * Blocks 0 and 1 of a 5.25" bootable Pascal disk, formatted by * APPLE3:FORMATTER from Pascal v1.3. */ const uint8_t gPascal525Block0[] = { 0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0, 0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24, 0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46, 0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x0d, 0xa9, 0x5c, 0x85, 0x0c, 0xad, 0x00, 0x08, 0xc9, 0x06, 0xb0, 0x0a, 0x69, 0x02, 0x8d, 0x00, 0x08, 0xe6, 0x3d, 0x6c, 0x0c, 0x00, 0xa9, 0x00, 0x8d, 0x78, 0x04, 0xa9, 0x0a, 0x85, 0x0e, 0xa9, 0x80, 0x85, 0x3f, 0x85, 0x11, 0xa9, 0x00, 0x85, 0x10, 0xa9, 0x08, 0x85, 0x02, 0xa9, 0x02, 0x85, 0x0f, 0xa9, 0x00, 0x20, 0x4c, 0x09, 0xa2, 0x4e, 0xa0, 0x06, 0xb1, 0x10, 0xd9, 0x39, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10, 0x69, 0x1a, 0x85, 0x10, 0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6, 0x0e, 0xd0, 0xcc, 0x20, 0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2, 0x00, 0xbd, 0x2a, 0x09, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15, 0xd0, 0xf3, 0xf0, 0xfe, 0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x81, 0xc0, 0xad, 0x81, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x30, 0x85, 0x02, 0xa0, 0x00, 0xb1, 0x10, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x20, 0x4c, 0x09, 0xad, 0x89, 0xc0, 0xa9, 0xd0, 0x85, 0x3f, 0xa9, 0x10, 0x85, 0x02, 0xa0, 0x00, 0xb1, 0x10, 0x18, 0x69, 0x18, 0x85, 0x0f, 0xc8, 0xb1, 0x10, 0x69, 0x00, 0x20, 0x4c, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad, 0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e, 0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb, 0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xfb, 0x08, 0x4e, 0x4f, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x4a, 0x08, 0xa5, 0x0f, 0x29, 0x07, 0x0a, 0x85, 0x00, 0xa5, 0x0f, 0x28, 0x6a, 0x4a, 0x4a, 0x85, 0xf0, 0xa9, 0x00, 0x85, 0x3e, 0x4c, 0x78, 0x09, 0xa6, 0x02, 0xf0, 0x22, 0xc6, 0x02, 0xe6, 0x3f, 0xe6, 0x00, 0xa5, 0x00, 0x49, 0x10, 0xd0, 0x04, 0x85, 0x00, 0xe6, 0xf0, 0xa4, 0x00, 0xb9, 0x8b, 0x09, 0x85, 0xf1, 0xa2, 0x00, 0xe4, 0x02, 0xf0, 0x05, 0x20, 0x9b, 0x09, 0x90, 0xda, 0x60, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x01, 0x03, 0x05, 0x07, 0x09, 0x0b, 0x0d, 0x0f, 0xa6, 0x43, 0xa5, 0xf0, 0x0a, 0x0e, 0x78, 0x04, 0x20, 0xa3, 0x0a, 0x4e, 0x78, 0x04, 0x20, 0x47, 0x0a, 0xb0, 0xfb, 0xa4, 0x2e, 0x8c, 0x78, 0x04, 0xc4, 0xf0, 0xd0, 0xe6, 0xa5, 0x2d, 0xc5, 0xf1, 0xd0, 0xec, 0x20, 0xdf, 0x09, 0xb0, 0xe7, 0x20, 0xc7, 0x09, 0x18, 0x60, 0xa0, 0x00, 0xa2, 0x56, 0xca, 0x30, 0xfb, 0xb9, 0x00, 0x02, 0x5e, 0x00, 0x03, 0x2a, 0x5e, 0x00, 0x03, 0x2a, 0x91, 0x3e, 0xc8, 0xd0, 0xed, 0x60, 0xa0, 0x20, 0x88, 0xf0, 0x61, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x49, 0xd5, 0xd0, 0xf4, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x56, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xad }; const uint8_t gPascal525Block1[] = { 0xd0, 0xe7, 0xa9, 0x00, 0x88, 0x84, 0x26, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00, 0x03, 0xd0, 0xee, 0x84, 0x26, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0x59, 0xd6, 0x02, 0xa4, 0x26, 0x99, 0x00, 0x02, 0xc8, 0xd0, 0xee, 0xbc, 0x8c, 0xc0, 0x10, 0xfb, 0xd9, 0xd6, 0x02, 0xd0, 0x13, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0x0a, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xf0, 0x5c, 0x38, 0x60, 0xa0, 0xfc, 0x84, 0x26, 0xc8, 0xd0, 0x04, 0xe6, 0x26, 0xf0, 0xf3, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xd5, 0xd0, 0xf0, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xf2, 0xa0, 0x03, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0x96, 0xd0, 0xe7, 0xa9, 0x00, 0x85, 0x27, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x2a, 0x85, 0x26, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0x25, 0x26, 0x99, 0x2c, 0x00, 0x45, 0x27, 0x88, 0x10, 0xe7, 0xa8, 0xd0, 0xb7, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xde, 0xd0, 0xae, 0xea, 0xbd, 0x8c, 0xc0, 0x10, 0xfb, 0xc9, 0xaa, 0xd0, 0xa4, 0x18, 0x60, 0x86, 0x2b, 0x85, 0x2a, 0xcd, 0x78, 0x04, 0xf0, 0x48, 0xa9, 0x00, 0x85, 0x26, 0xad, 0x78, 0x04, 0x85, 0x27, 0x38, 0xe5, 0x2a, 0xf0, 0x37, 0xb0, 0x07, 0x49, 0xff, 0xee, 0x78, 0x04, 0x90, 0x05, 0x69, 0xfe, 0xce, 0x78, 0x04, 0xc5, 0x26, 0x90, 0x02, 0xa5, 0x26, 0xc9, 0x0c, 0xb0, 0x01, 0xa8, 0x20, 0xf4, 0x0a, 0xb9, 0x15, 0x0b, 0x20, 0x04, 0x0b, 0xa5, 0x27, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa, 0xbd, 0x80, 0xc0, 0xb9, 0x21, 0x0b, 0x20, 0x04, 0x0b, 0xe6, 0x26, 0xd0, 0xbf, 0x20, 0x04, 0x0b, 0xad, 0x78, 0x04, 0x29, 0x03, 0x0a, 0x05, 0x2b, 0xaa, 0xbd, 0x81, 0xc0, 0xa6, 0x2b, 0x60, 0xea, 0xa2, 0x11, 0xca, 0xd0, 0xfd, 0xe6, 0x46, 0xd0, 0x02, 0xe6, 0x47, 0x38, 0xe9, 0x01, 0xd0, 0xf0, 0x60, 0x01, 0x30, 0x28, 0x24, 0x20, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x70, 0x2c, 0x26, 0x22, 0x1f, 0x1e, 0x1d, 0x1c, 0x1c, 0x1c, 0x1c, 0x1c, 0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c, 0x45, 0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x03, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbb }; /* * Block 0 of a 3.5" bootable Pascal disk, formatted by * APPLE3:FORMATTER from Pascal v1.3. Block 1 is zeroed out. */ const uint8_t gPascal35Block0[] = { 0x01, 0xe0, 0x70, 0xb0, 0x04, 0xe0, 0x40, 0xb0, 0x39, 0xbd, 0x88, 0xc0, 0x20, 0x20, 0x08, 0xa2, 0x00, 0xbd, 0x25, 0x08, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x1d, 0xd0, 0xf3, 0xf0, 0xfe, 0xa9, 0x0a, 0x4c, 0x24, 0xfc, 0x4d, 0x55, 0x53, 0x54, 0x20, 0x42, 0x4f, 0x4f, 0x54, 0x20, 0x46, 0x52, 0x4f, 0x4d, 0x20, 0x53, 0x4c, 0x4f, 0x54, 0x20, 0x34, 0x2c, 0x20, 0x35, 0x20, 0x4f, 0x52, 0x20, 0x36, 0x8a, 0x85, 0x43, 0x4a, 0x4a, 0x4a, 0x4a, 0x09, 0xc0, 0x85, 0x15, 0x8d, 0x5d, 0x09, 0xa9, 0x00, 0x8d, 0x78, 0x04, 0x85, 0x14, 0xa9, 0x0a, 0x85, 0x0e, 0xa9, 0x80, 0x85, 0x13, 0x85, 0x11, 0xa9, 0x00, 0x85, 0x10, 0x85, 0x0b, 0xa9, 0x02, 0x85, 0x0a, 0xa9, 0x04, 0x85, 0x02, 0x20, 0x40, 0x09, 0xa2, 0x4e, 0xa0, 0x06, 0xb1, 0x10, 0xd9, 0x2d, 0x09, 0xf0, 0x2b, 0x18, 0xa5, 0x10, 0x69, 0x1a, 0x85, 0x10, 0x90, 0x02, 0xe6, 0x11, 0xca, 0xd0, 0xe9, 0xc6, 0x0e, 0xd0, 0xcc, 0x20, 0x20, 0x08, 0xa6, 0x43, 0xbd, 0x88, 0xc0, 0xa2, 0x00, 0xbd, 0x1e, 0x09, 0x09, 0x80, 0x20, 0xfd, 0xfb, 0xe8, 0xe0, 0x15, 0xd0, 0xf3, 0xf0, 0xfe, 0xc8, 0xc0, 0x13, 0xd0, 0xc9, 0xad, 0x83, 0xc0, 0xad, 0x83, 0xc0, 0xa9, 0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x85, 0x0a, 0xc8, 0xb1, 0x10, 0x85, 0x0b, 0xa9, 0x18, 0x85, 0x02, 0x20, 0x40, 0x09, 0xad, 0x8b, 0xc0, 0xa9, 0xd0, 0x85, 0x13, 0xa0, 0x00, 0xb1, 0x10, 0x18, 0x69, 0x18, 0x85, 0x0a, 0xc8, 0xb1, 0x10, 0x69, 0x00, 0x85, 0x0b, 0xa9, 0x08, 0x85, 0x02, 0x20, 0x40, 0x09, 0xa5, 0x43, 0xc9, 0x50, 0xf0, 0x08, 0x90, 0x1a, 0xad, 0x80, 0xc0, 0x6c, 0xf8, 0xff, 0xa2, 0x00, 0x8e, 0xc4, 0xfe, 0xe8, 0x8e, 0xc6, 0xfe, 0xe8, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef, 0x08, 0xa2, 0x00, 0x8e, 0xc0, 0xfe, 0xe8, 0x8e, 0xc2, 0xfe, 0xa2, 0x04, 0x8e, 0xb6, 0xfe, 0xe8, 0x8e, 0xb8, 0xfe, 0x4c, 0xef, 0x08, 0x4e, 0x4f, 0x20, 0x46, 0x49, 0x4c, 0x45, 0x20, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x0c, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, 0x2e, 0x41, 0x50, 0x50, 0x4c, 0x45, 0xa9, 0x01, 0x85, 0x42, 0xa0, 0xff, 0xb1, 0x14, 0x8d, 0x5c, 0x09, 0xa9, 0x00, 0x85, 0x44, 0xa5, 0x13, 0x85, 0x45, 0xa5, 0x0a, 0x85, 0x46, 0xa5, 0x0b, 0x85, 0x47, 0x20, 0x00, 0x00, 0x90, 0x03, 0x4c, 0x5b, 0x08, 0xc6, 0x02, 0xf0, 0x0c, 0xe6, 0x13, 0xe6, 0x13, 0xe6, 0x0a, 0xd0, 0xdc, 0xe6, 0x0b, 0xd0, 0xd8, 0x60, 0x20, 0x43, 0x4f, 0x50, 0x59, 0x52, 0x49, 0x47, 0x48, 0x54, 0x20, 0x41, 0x50, 0x50, 0x4c, 0x45, 0x20, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x52, 0x2c, 0x20, 0x49, 0x4e, 0x43, 0x2e, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x34, 0x2c, 0x20, 0x31, 0x39, 0x38, 0x35, 0x20, 0x43, 0x2e, 0x4c, 0x45, 0x55, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0x01, 0x00, 0x00, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; /* * Write the Pascal boot blocks onto the disk image. */ DIError DiskFSPascal::WriteBootBlocks(void) { DIError dierr; uint8_t block0[512]; uint8_t block1[512]; bool is525 = false; assert(fpImg->GetHasBlocks()); if (fpImg->GetNumBlocks() == 280) is525 = true; else if (fpImg->GetNumBlocks() == 1600) is525 = false; else { LOGI(" Pascal boot blocks for blocks=%ld unknown", fpImg->GetNumBlocks()); return kDIErrInternal; } if (is525) { memcpy(block0, gPascal525Block0, sizeof(block0)); memcpy(block1, gPascal525Block1, sizeof(block1)); } else { memcpy(block0, gPascal35Block0, 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; } /* * Scan for damaged files and conflicting file allocation entries. * * Appends some entries to the DiskImg notes, so this should only be run * once per DiskFS. * * Returns "true" if disk appears to be perfect, "false" otherwise. */ bool DiskFSPascal::CheckDiskIsGood(void) { //DIError dierr; bool result = true; if (fEarlyDamage) result = false; /* (don't need to check to see if the boot blocks or disk catalog are marked in use -- the directory is defined by the set of blocks in the volume header) */ /* * 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; } return result; } /* * Run through the list of files and count up the free blocks. */ DIError DiskFSPascal::GetFreeSpaceCount(long* pTotalUnits, long* pFreeUnits, int* pUnitSize) const { A2FilePascal* pFile; long freeBlocks = 0; uint16_t prevNextBlock = fNextBlock; pFile = (A2FilePascal*) GetNextFile(NULL); while (pFile != NULL) { freeBlocks += pFile->fStartBlock - prevNextBlock; prevNextBlock = pFile->fNextBlock; pFile = (A2FilePascal*) GetNextFile(pFile); } freeBlocks += fTotalBlocks - prevNextBlock; *pTotalUnits = fTotalBlocks; *pFreeUnits = freeBlocks; *pUnitSize = kBlockSize; return kDIErrNone; } /* * Normalize a Pascal path. Used when adding files from DiskArchive. * * "*pNormalizedBufLen" is used to pass in the length of the buffer and * pass out the length of the string (should the buffer prove inadequate). */ DIError DiskFSPascal::NormalizePath(const char* path, char fssep, char* normalizedBuf, int* pNormalizedBufLen) { DIError dierr = kDIErrNone; char tmpBuf[A2FilePascal::kMaxFileName+1]; int len; DoNormalizePath(path, fssep, tmpBuf); len = strlen(tmpBuf)+1; if (*pNormalizedBufLen < len) dierr = kDIErrDataOverrun; else strcpy(normalizedBuf, tmpBuf); *pNormalizedBufLen = len; return dierr; } /* * Normalize a Pascal pathname. Lower case becomes upper case, invalid * characters get stripped. * * "outBuf" must be able to hold kMaxFileName+1 characters. */ void DiskFSPascal::DoNormalizePath(const char* name, char fssep, char* outBuf) { char* outp = outBuf; const char* cp; /* throw out leading pathname, if any */ if (fssep != '\0') { cp = strrchr(name, fssep); if (cp != NULL) name = cp+1; } while (*name != '\0' && (outp - outBuf) < A2FilePascal::kMaxFileName) { if (*name > 0x20 && *name < 0x7f && strchr(kInvalidNameChars, *name) == NULL) { *outp++ = toupper(*name); } name++; } *outp = '\0'; if (*outBuf == '\0') { /* nothing left */ strcpy(outBuf, "BLANK"); } } /* * Create an empty file. It doesn't look like pascal normally allows you * to create a zero-block file, so we create a 1-block file and set the * "data in last block" field to zero. * * We don't know how big the file will be, so we can't do a "best fit" * algorithm for placement. Instead, we just put it in the largest * available free space. * * NOTE: the Pascal system will auto-delete zero-byte files. It expects a * brand-new 1-block file to have a "bytes remaining" of 512. The files * we create here are expected to be written to, not used as filler, so * this behavior is actually a *good* thing. */ DIError DiskFSPascal::CreateFile(const CreateParms* pParms, A2File** ppNewFile) { DIError dierr = kDIErrNone; const bool createUnique = (GetParameter(kParm_CreateUnique) != 0); char normalName[A2FilePascal::kMaxFileName+1]; A2FilePascal* pNewFile = NULL; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; assert(pParms != NULL); assert(pParms->pathName != NULL); assert(pParms->storageType == A2FileProDOS::kStorageSeedling); LOGI(" Pascal ---v--- CreateFile '%s'", pParms->pathName); /* compute maxFiles, which includes the vol dir header */ int maxFiles = ((fNextBlock - kVolHeaderBlock) * kBlkSize) / kDirectoryEntryLen; if (fNumFiles >= maxFiles-1) { LOGI("Pascal volume directory full (%d entries)", fNumFiles); return kDIErrVolumeDirFull; } *ppNewFile = NULL; DoNormalizePath(pParms->pathName, pParms->fssep, normalName); /* * See if the file already exists. * * If "create unique" is set, we append digits until the name doesn't * match any others. The name will be modified in place. */ if (createUnique) { MakeFileNameUnique(normalName); } else { if (GetFileByName(normalName) != NULL) { LOGI(" Pascal create: normalized name '%s' already exists", normalName); dierr = kDIErrFileExists; goto bail; } } /* * Find the largest gap in the file space. * * We get an index pointer and A2File pointer to the previous entry. If * the blank space is at the head of the list, prevIdx will be zero and * pPrevFile will be NULL. */ A2FilePascal* pPrevFile; int prevIdx; dierr = FindLargestFreeArea(&prevIdx, &pPrevFile); if (dierr != kDIErrNone) goto bail; assert(prevIdx >= 0); /* * Make a new entry. */ time_t now; pNewFile = new A2FilePascal(this); if (pNewFile == NULL) { dierr = kDIErrMalloc; goto bail; } if (pPrevFile == NULL) pNewFile->fStartBlock = fNextBlock; else pNewFile->fStartBlock = pPrevFile->fNextBlock; pNewFile->fNextBlock = pNewFile->fStartBlock +1; // alloc 1 block pNewFile->fFileType = A2FilePascal::ConvertFileType(pParms->fileType); memset(pNewFile->fFileName, 0, A2FilePascal::kMaxFileName); strcpy(pNewFile->fFileName, normalName); pNewFile->fBytesRemaining = 0; now = time(NULL); pNewFile->fModWhen = A2FilePascal::ConvertPascalDate(now); pNewFile->fLength = 0; /* * Make a hole. */ dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; if (fNumFiles > prevIdx) { LOGI(" Pascal sliding last %d entries down a slot", fNumFiles - prevIdx); memmove(fDirectory + (prevIdx+2) * kDirectoryEntryLen, fDirectory + (prevIdx+1) * kDirectoryEntryLen, (fNumFiles - prevIdx) * kDirectoryEntryLen); } /* * Fill the hole. */ uint8_t* dirPtr; dirPtr = fDirectory + (prevIdx+1) * kDirectoryEntryLen; PutShortLE(&dirPtr[0x00], pNewFile->fStartBlock); PutShortLE(&dirPtr[0x02], pNewFile->fNextBlock); PutShortLE(&dirPtr[0x04], (uint16_t) pNewFile->fFileType); dirPtr[0x06] = (uint8_t) strlen(pNewFile->fFileName); memcpy(&dirPtr[0x07], pNewFile->fFileName, A2FilePascal::kMaxFileName); PutShortLE(&dirPtr[0x16], pNewFile->fBytesRemaining); PutShortLE(&dirPtr[0x18], pNewFile->fModWhen); /* * Update the #of files. */ fNumFiles++; PutShortLE(&fDirectory[0x10], fNumFiles); /* * Flush. */ dierr = SaveCatalog(); if (dierr != kDIErrNone) goto bail; /* * Add to the linear file list. */ InsertFileInList(pNewFile, pPrevFile); *ppNewFile = pNewFile; pNewFile = NULL; bail: delete pNewFile; FreeCatalog(); return dierr; } /* * Make the name pointed to by "fileName" unique. The name should already * be FS-normalized, and be in a buffer that can hold at least kMaxFileName+1 * bytes. * * (This is nearly identical to the code in the ProDOS implementation. I'd * like to make it a general DiskFS function, but making the loop condition * work requires setting up callbacks, which isn't hard here but is a little * annoying in ProDOS because of the subdir buffer. So it's cut & paste * for now.) * * Returns an error on failure, which should be impossible. */ DIError DiskFSPascal::MakeFileNameUnique(char* fileName) { assert(fileName != NULL); assert(strlen(fileName) <= A2FilePascal::kMaxFileName); if (GetFileByName(fileName) == NULL) return kDIErrNone; LOGI(" Pascal 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, (A2FilePascal::kMaxFileName - nameLen) +1); /* * If this has what looks like a filename extension, grab it. We want * to preserve ".gif", ".c", etc. */ const char* cp = strrchr(fileName, '.'); if (cp != NULL) { int tmpOffset = cp - fileName; if (tmpOffset > 0 && nameLen - tmpOffset <= kMaxExtensionLen) { LOGI(" Pascal (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 > A2FilePascal::kMaxFileName) copyOffset = A2FilePascal::kMaxFileName - dotLen - digitLen; else copyOffset = nameLen - dotLen; memcpy(fileName + copyOffset, digitBuf, digitLen); if (dotLen != 0) memcpy(fileName + copyOffset + digitLen, dotBuf, dotLen); } while (GetFileByName(fileName) != NULL); LOGI(" Pascal converted to unique name: %s", fileName); return kDIErrNone; } /* * Find the largest chunk of free space on the disk. * * Returns the index to the directory entry of the file immediately before * the chunk (where 0 is the directory header), and the corresponding * A2File entry. * * If there's no free space left, returns kDIErrDiskFull. */ DIError DiskFSPascal::FindLargestFreeArea(int *pPrevIdx, A2FilePascal** ppPrevFile) { A2FilePascal* pFile; A2FilePascal* pPrevFile; uint16_t prevNextBlock = fNextBlock; int gapSize, maxGap, maxIndex, idx; maxIndex = -1; maxGap = 0; idx = 0; *ppPrevFile = pPrevFile = NULL; pFile = (A2FilePascal*) GetNextFile(NULL); while (pFile != NULL) { gapSize = pFile->fStartBlock - prevNextBlock; if (gapSize > maxGap) { maxGap = gapSize; maxIndex = idx; *ppPrevFile = pPrevFile; } idx++; prevNextBlock = pFile->fNextBlock; pPrevFile = pFile; pFile = (A2FilePascal*) GetNextFile(pFile); } gapSize = fTotalBlocks - prevNextBlock; if (gapSize > maxGap) { maxGap = gapSize; maxIndex = idx; *ppPrevFile = pPrevFile; } LOGI("Pascal largest gap after entry %d '%s' (size=%d)", maxIndex, *ppPrevFile != NULL ? (*ppPrevFile)->GetPathName() : "(root)", maxGap); *pPrevIdx = maxIndex; if (maxIndex < 0) return kDIErrDiskFull; return kDIErrNone; } /* * Delete a file. Because Pascal doesn't have a block allocation map, this * is a simple matter of crunching the directory entry out. */ DIError DiskFSPascal::DeleteFile(A2File* pGenericFile) { DIError dierr = kDIErrNone; A2FilePascal* pFile = (A2FilePascal*) pGenericFile; uint8_t* pEntry; int dirLen, offsetToNextEntry; if (pGenericFile == NULL) { assert(false); return kDIErrInvalidArg; } if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; if (pGenericFile->IsFileOpen()) return kDIErrFileOpen; LOGI(" Pascal deleting '%s'", pFile->GetPathName()); dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; pEntry = FindDirEntry(pFile); if (pEntry == NULL) { assert(false); dierr = kDIErrInternal; goto bail; } dirLen = (fNumFiles+1) * kDirectoryEntryLen; offsetToNextEntry = (pEntry - fDirectory) + kDirectoryEntryLen; if (dirLen == offsetToNextEntry) { LOGI("+++ removing last entry"); } else { memmove(pEntry, pEntry+kDirectoryEntryLen, dirLen - offsetToNextEntry); } assert(fNumFiles > 0); fNumFiles--; PutShortLE(&fDirectory[0x10], fNumFiles); dierr = SaveCatalog(); if (dierr != kDIErrNone) goto bail; /* * Remove the A2File* from the list. */ DeleteFileFromList(pFile); bail: FreeCatalog(); return dierr; } /* * Rename a file. */ DIError DiskFSPascal::RenameFile(A2File* pGenericFile, const char* newName) { DIError dierr = kDIErrNone; A2FilePascal* pFile = (A2FilePascal*) pGenericFile; char normalName[A2FilePascal::kMaxFileName+1]; uint8_t* pEntry; if (pFile == NULL || newName == NULL) return kDIErrInvalidArg; if (!IsValidFileName(newName)) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; /* not strictly necessary, but watch sanity check in Close/FindDirEntry */ if (pGenericFile->IsFileOpen()) return kDIErrFileOpen; DoNormalizePath(newName, '\0', normalName); LOGI(" Pascal renaming '%s' to '%s'", pFile->GetPathName(), normalName); dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; pEntry = FindDirEntry(pFile); if (pEntry == NULL) { assert(false); dierr = kDIErrInternal; goto bail; } pEntry[0x06] = (uint8_t)strlen(normalName); memcpy(&pEntry[0x07], normalName, A2FilePascal::kMaxFileName); strcpy(pFile->fFileName, normalName); dierr = SaveCatalog(); if (dierr != kDIErrNone) goto bail; bail: FreeCatalog(); return dierr; } /* * Set file info. * * Pascal does not have an aux type or access flags. It has a file type, * but we don't allow the full range of ProDOS types. Attempting to change * to an unsupported type results in "PDA" being used. */ DIError DiskFSPascal::SetFileInfo(A2File* pGenericFile, uint32_t fileType, uint32_t auxType, uint32_t accessFlags) { DIError dierr = kDIErrNone; A2FilePascal* pFile = (A2FilePascal*) pGenericFile; uint8_t* pEntry; if (pFile == NULL) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; if (!fDiskIsGood) return kDIErrBadDiskImage; LOGI("Pascal SetFileInfo '%s' fileType=0x%04x", pFile->GetPathName(), fileType); dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; pEntry = FindDirEntry(pFile); if (pEntry == NULL) { assert(false); dierr = kDIErrInternal; goto bail; } A2FilePascal::FileType newType; newType = A2FilePascal::ConvertFileType(fileType); PutShortLE(&pEntry[0x04], (uint16_t) newType); dierr = SaveCatalog(); if (dierr != kDIErrNone) goto bail; /* update our local copy */ pFile->fFileType = newType; bail: FreeCatalog(); return dierr; } /* * Change the Pascal volume name. */ DIError DiskFSPascal::RenameVolume(const char* newName) { DIError dierr = kDIErrNone; char normalName[A2FilePascal::kMaxFileName+1]; if (!IsValidVolumeName(newName)) return kDIErrInvalidArg; if (fpImg->GetReadOnly()) return kDIErrAccessDenied; DoNormalizePath(newName, '\0', normalName); dierr = LoadCatalog(); if (dierr != kDIErrNone) goto bail; fDirectory[0x06] = (uint8_t)strlen(normalName); memcpy(&fDirectory[0x07], normalName, fDirectory[0x06]); strcpy(fVolumeName, normalName); SetVolumeID(); dierr = SaveCatalog(); if (dierr != kDIErrNone) goto bail; bail: FreeCatalog(); return dierr; } /* * Find "pFile" in "fDirectory". */ uint8_t* DiskFSPascal::FindDirEntry(A2FilePascal* pFile) { uint8_t* ptr; int i; assert(fDirectory != NULL); ptr = fDirectory; // volume header; first iteration skips over it for (i = 0; i < fNumFiles; i++) { ptr += kDirectoryEntryLen; if (GetShortLE(&ptr[0x00]) == pFile->fStartBlock) { if (memcmp(&ptr[0x07], pFile->fFileName, ptr[0x06]) != 0) { assert(false); LOGI("name/block mismatch on '%s' %d", pFile->GetPathName(), pFile->fStartBlock); return NULL; } return ptr; } } return NULL; } /* * =========================================================================== * A2FilePascal * =========================================================================== */ /* * Convert Pascal file type to ProDOS file type. */ uint32_t A2FilePascal::GetFileType(void) const { switch (fFileType) { case kTypeUntyped: return 0x00; // NON case kTypeXdsk: return 0x01; // BAD (was 0xf2 in v1.2.2) case kTypeCode: return 0x02; // PCD case kTypeText: return 0x03; // PTX case kTypeInfo: return 0xf3; // no idea case kTypeData: return 0x05; // PDA case kTypeGraf: return 0xf4; // no idea case kTypeFoto: return 0x08; // FOT case kTypeSecurdir: return 0xf5; // no idea default: LOGI("Pascal WARNING: found invalid file type %d", fFileType); return 0; } } /* * Convert a ProDOS file type to a Pascal file type. */ /*static*/ A2FilePascal::FileType A2FilePascal::ConvertFileType(long prodosType) { FileType newType; switch (prodosType) { case 0x00: newType = kTypeUntyped; break; // NON case 0x01: newType = kTypeXdsk; break; // BAD case 0x02: newType = kTypeCode; break; // PCD case 0x03: newType = kTypeText; break; // PTX case 0xf3: newType = kTypeInfo; break; // ? case 0x05: newType = kTypeData; break; // PDA case 0xf4: newType = kTypeGraf; break; // ? case 0x08: newType = kTypeFoto; break; // FOT case 0xf5: newType = kTypeSecurdir; break; // ? default: newType = kTypeData; break; // PDA for generic } return newType; } /* * Convert from Pascal compact date format to a time_t. * * Format yyyyyyydddddmmmm * Month 0..12 (0 indicates invalid date) * Day 0..31 * Year 0..100 (1900-1999; 100 will be rejected) * * We follow the ProDOS protocol of "year < 40 == 1900 + year". We could * probably make that 1970, but the time_t epoch ends before then. * * The Pascal Filer uses a special date with the year 100 in it to indicate * file updates in progress. If the system comes up and sees a file with * the year 100, it will assume that the file was created shortly before the * system crashed, and will remove the file. */ /*static*/ time_t A2FilePascal::ConvertPascalDate(PascalDate pascalDate) { int year, month, day; month = pascalDate & 0x0f; if (!month) return 0; day = (pascalDate >> 4) & 0x1f; year = (pascalDate >> 9) & 0x7f; if (year == 100) { // ought to mark the file as "suspicious"? LOGI("Pascal WARNING: date with year=100"); } if (year < 40) year += 100; struct tm tmbuf; time_t when; tmbuf.tm_sec = 0; tmbuf.tm_min = 0; tmbuf.tm_hour = 0; tmbuf.tm_mday = day; tmbuf.tm_mon = month-1; 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 Pascal-format date. * * CiderPress uses kDateInvalid==-1 and kDateNone==-2. */ /*static*/ A2FilePascal::PascalDate A2FilePascal::ConvertPascalDate(time_t unixDate) { uint32_t date, year; struct tm* ptm; 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; // years since 1900 if (year >= 100) year -= 100; if (year < 0 || year >= 100) { LOGW("WHOOPS: got year %u from %d", year, ptm->tm_year); year = 70; } date = year << 9 | (ptm->tm_mon+1) | ptm->tm_mday << 4; return (PascalDate) date; } /* * Return the file modification date. */ time_t A2FilePascal::GetModWhen(void) const { return ConvertPascalDate(fModWhen); } /* * Dump the contents of the A2File structure. */ void A2FilePascal::Dump(void) const { LOGI("A2FilePascal '%s'", fFileName); LOGI(" start=%d next=%d type=%d", fStartBlock, fNextBlock, fFileType); LOGI(" bytesRem=%d modWhen=0x%04x", fBytesRemaining, fModWhen); } /* * Not a whole lot to do, since there's no fancy index blocks. */ DIError A2FilePascal::Open(A2FileDescr** ppOpenFile, bool readOnly, bool rsrcFork /*=false*/) { A2FDPascal* pOpenFile = NULL; if (!readOnly) { if (fpDiskFS->GetDiskImg()->GetReadOnly()) return kDIErrAccessDenied; if (fpDiskFS->GetFSDamaged()) return kDIErrBadDiskImage; } if (fpOpenFile != NULL) return kDIErrAlreadyOpen; if (rsrcFork) return kDIErrForkNotFound; pOpenFile = new A2FDPascal(this); pOpenFile->fOffset = 0; pOpenFile->fOpenEOF = fLength; pOpenFile->fOpenBlocksUsed = fNextBlock - fStartBlock; pOpenFile->fModified = false; fpOpenFile = pOpenFile; *ppOpenFile = pOpenFile; return kDIErrNone; } /* * =========================================================================== * A2FDPascal * =========================================================================== */ /* * Read a chunk of data from the current offset. */ DIError A2FDPascal::Read(void* buf, size_t len, size_t* pActual) { LOGD(" Pascal reading %lu bytes from '%s' (offset=%ld)", (unsigned long) len, fpFile->GetPathName(), (long) fOffset); A2FilePascal* pFile = (A2FilePascal*) fpFile; /* don't allow them to read past the end of the file */ if (fOffset + (long)len > fOpenEOF) { if (pActual == NULL) return kDIErrDataUnderrun; len = (size_t) (fOpenEOF - fOffset); } if (pActual != NULL) *pActual = len; long incrLen = len; DIError dierr = kDIErrNone; uint8_t blkBuf[kBlkSize]; long block = pFile->fStartBlock + (long) (fOffset / kBlkSize); int bufOffset = (long) (fOffset % kBlkSize); // (& 0x01ff) size_t thisCount; if (len == 0) return kDIErrNone; assert(fOpenEOF != 0); while (len) { assert(block >= pFile->fStartBlock && block < pFile->fNextBlock); dierr = pFile->GetDiskFS()->GetDiskImg()->ReadBlock(block, blkBuf); if (dierr != kDIErrNone) { LOGI(" Pascal error reading file '%s'", pFile->fFileName); return dierr; } thisCount = kBlkSize - bufOffset; if (thisCount > len) thisCount = len; memcpy(buf, blkBuf + bufOffset, thisCount); len -= thisCount; buf = (char*)buf + thisCount; bufOffset = 0; block++; } fOffset += incrLen; return dierr; } /* * Write data at the current offset. * * We make the customary assumptions here: we're writing to a brand-new file, * and writing all data in one shot. On a Pascal disk, that makes this * process almost embarrassingly simple. */ DIError A2FDPascal::Write(const void* buf, size_t len, size_t* pActual) { DIError dierr = kDIErrNone; A2FilePascal* pFile = (A2FilePascal*) fpFile; DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS(); uint8_t blkBuf[kBlkSize]; size_t origLen = len; LOGD(" DOS Write len=%lu %s", (unsigned long) len, pFile->GetPathName()); if (len >= 0x01000000) { // 16MB assert(false); return kDIErrInvalidArg; } assert(fOffset == 0); // big simplifying assumption assert(fOpenEOF == 0); // another one assert(fOpenBlocksUsed == 1); assert(buf != NULL); /* * Verify that there's enough room between this file and the next to * hold the contents of the file. */ long blocksNeeded, blocksAvail; A2FilePascal* pNextFile; pNextFile = (A2FilePascal*) pDiskFS->GetNextFile(pFile); if (pNextFile == NULL) blocksAvail = pDiskFS->GetTotalBlocks() - pFile->fStartBlock; else blocksAvail = pNextFile->fStartBlock - pFile->fStartBlock; blocksNeeded = (len + kBlkSize -1) / kBlkSize; LOGD("Pascal write '%s' %lu bytes: avail=%ld needed=%ld", pFile->GetPathName(), (unsigned long) len, blocksAvail, blocksNeeded); if (blocksAvail < blocksNeeded) return kDIErrDiskFull; /* * Write the data. */ long block; block = pFile->fStartBlock; while (len != 0) { if (len >= (size_t) kBlkSize) { /* full block write */ dierr = pDiskFS->GetDiskImg()->WriteBlock(block, buf); if (dierr != kDIErrNone) goto bail; len -= kBlkSize; buf = (uint8_t*) buf + kBlkSize; } else { /* partial block write */ memset(blkBuf, 0, sizeof(blkBuf)); memcpy(blkBuf, buf, len); dierr = pDiskFS->GetDiskImg()->WriteBlock(block, blkBuf); if (dierr != kDIErrNone) goto bail; len = 0; } block++; } /* * Update FD state. */ fOpenBlocksUsed = blocksNeeded; fOpenEOF = origLen; fOffset = origLen; fModified = true; bail: return dierr; } /* * Seek to a new offset. */ DIError A2FDPascal::Seek(di_off_t offset, DIWhence whence) { //di_off_t fileLen = ((A2FilePascal*) fpFile)->fLength; 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 kDIErrNone; } /* * Return current offset. */ di_off_t A2FDPascal::Tell(void) { return fOffset; } /* * Release file state, and tell our parent to destroy us. * * 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 A2FDPascal::Close(void) { DIError dierr = kDIErrNone; DiskFSPascal* pDiskFS = (DiskFSPascal*) fpFile->GetDiskFS(); if (fModified) { A2FilePascal* pFile = (A2FilePascal*) fpFile; uint8_t* pEntry; dierr = pDiskFS->LoadCatalog(); if (dierr != kDIErrNone) goto bail; /* * Update our internal copies of stuff. */ pFile->fLength = fOpenEOF; pFile->fNextBlock = pFile->fStartBlock + (uint16_t) fOpenBlocksUsed; pFile->fModWhen = A2FilePascal::ConvertPascalDate(time(NULL)); /* * Update the "next block" value and the length-in-last-block. We * have to scan through the directory to find our entry, rather * than remember an offset at "open" time, on the off chance that * somebody created or deleted a file after we were opened. */ pEntry = pDiskFS->FindDirEntry(pFile); if (pEntry == NULL) { // we deleted an open file? assert(false); dierr = kDIErrInternal; goto bail; } uint16_t bytesInLastBlock; bytesInLastBlock = (uint16_t)pFile->fLength % kBlkSize; if (bytesInLastBlock == 0) bytesInLastBlock = 512; // exactly filled out last block PutShortLE(&pEntry[0x02], pFile->fNextBlock); PutShortLE(&pEntry[0x16], bytesInLastBlock); PutShortLE(&pEntry[0x18], pFile->fModWhen); dierr = pDiskFS->SaveCatalog(); if (dierr != kDIErrNone) goto bail; } bail: pDiskFS->FreeCatalog(); fpFile->CloseDescr(this); return dierr; } /* * Return the #of sectors/blocks in the file. */ long A2FDPascal::GetSectorCount(void) const { A2FilePascal* pFile = (A2FilePascal*) fpFile; return (pFile->fNextBlock - pFile->fStartBlock) * 2; } long A2FDPascal::GetBlockCount(void) const { A2FilePascal* pFile = (A2FilePascal*) fpFile; return pFile->fNextBlock - pFile->fStartBlock; } /* * Return the Nth track/sector in this file. */ DIError A2FDPascal::GetStorage(long sectorIdx, long* pTrack, long* pSector) const { A2FilePascal* pFile = (A2FilePascal*) fpFile; long pascalIdx = sectorIdx / 2; long pascalBlock = pFile->fStartBlock + pascalIdx; if (pascalBlock >= pFile->fNextBlock) return kDIErrInvalidIndex; /* sparse blocks not possible on Pascal volumes */ BlockToTrackSector(pascalBlock, (sectorIdx & 0x01) != 0, pTrack, pSector); return kDIErrNone; } /* * Return the Nth 512-byte block in this file. */ DIError A2FDPascal::GetStorage(long blockIdx, long* pBlock) const { A2FilePascal* pFile = (A2FilePascal*) fpFile; long pascalBlock = pFile->fStartBlock + blockIdx; if (pascalBlock >= pFile->fNextBlock) return kDIErrInvalidIndex; *pBlock = pascalBlock; assert(*pBlock < pFile->GetDiskFS()->GetDiskImg()->GetNumBlocks()); return kDIErrNone; }