(* Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details *) program partmgr; const MaxPartitions = 32; LastPartBlock = 7; PartsPerBlock = 8; var partTable:array[0..LastPartBlock] of PartitionTableBlock; changed:array[0..LastPartBlock] of boolean; detectedCardSize:integer; cmd:char; done:boolean; lastPartNo:integer; function flags2str(flags:PartFlags):string; begin flags2str := ''; if PartEnabled in flags then flags2str := flags2str + 'E '; if PartBoot in flags then flags2str := flags2str + 'B '; if PartLast in flags then flags2str := flags2str + 'L '; if PartPhysical in flags then flags2str := flags2str + 'P '; if PartDefault in flags then flags2str := flags2str + 'D '; end; function str2flags(var s:string):PartFlags; begin str2flags := []; if 'E' in s then str2flags := str2flags + [PartEnabled]; if 'B' in s then str2flags := str2flags + [PartBoot]; if 'L' in s then str2flags := str2flags + [PartLast]; if 'P' in s then str2flags := str2flags + [PartPhysical]; if 'D' in s then str2flags := str2flags + [PartDefault]; end; function sanitizeName(var name:string):string; begin if (length(name) <= 32) and (maxlength(name) = 32) then sanitizeName := name else sanitizeName := ''; end; procedure changeNumber(prompt:string; var num:integer); var buf:string; err:integer; digits:string; begin str(num, digits); buf := prompt + ' [' + digits + ']> '; write(buf:30); readln(buf); val(buf,num,err); end; procedure readPartTable; var done:boolean; curblk:integer; error:integer; devid:integer; begin done := false; curblk := 0; devid := 0; (* we only support one device *) while not done do begin changed[curBlk] := false; readPartBlk(curblk, partTable[curblk], error, devid); if error <> 0 then begin done := true; writeln('Error ', error, ' reading partition block ', curblk); end else curblk := curblk + 1; if curBlk > LastPartBlock then done := true; end; end; procedure writePartBlock(no:integer); var error:integer; devid:integer; begin devid := 0; writePartBlk(no, partTable[no], error, devid); if error <> 0 then writeln('Error ', error, ' reading partition block ', no); end; procedure writePartitions; var blkNo:integer; begin for blkNo := 0 to LastPartBlock do begin if changed[blkNo] then begin writeln('Writing back partition block ',blkNo); writePartBlock(blkNo); end; end; end; function getPartition(partNo:integer):Partition; var blkNo:integer; begin blkNo := partNo div PartsPerBlock; if (blkNo < 0) or (blkNo > LastPartBlock) then writeln('internal error: invalid part no in getPartition') else getPartition := partTable[blkNo][partNo mod PartsPerBlock]; { writeln('** getPartition: ', blkNo, ' ', partNo mod PartsPerBlock); } end; procedure putPartition(var part:Partition; partNo:integer); var blkNo:integer; begin blkNo := partNo div PartsPerBlock; { writeln('** putPartition: ', blkNo, ' ', partNo mod PartsPerBlock); } if (blkNo < 0) or (blkNo > LastPartBlock) then writeln('internal error: invalid part no in getPartition') else begin partTable[blkNo][partNo mod PartsPerBlock] := part; changed[blkNo] := true; end; end; function isEmptyPart(var part:Partition):boolean; begin isEmptyPart := (part.startBlock = 0) and (part.blocks = 0); end; procedure printPartTable; var blkNo, partNo:integer; partBlk:PartitionTableBlock; part:Partition; totalPartNo:integer; begin totalPartNo := 0; writeln('Partition Table:'); writeln('No. ', 'Flags':11, 'Name':32, 'Start':10, 'Size':10); for blkNo := 0 to LastPartBlock do begin partBlk := partTable[blkNo]; for partNo := 0 to PartsPerBlock - 1 do begin part := partBlk[partNo]; if not isEmptyPart(part) then begin write(totalPartNo:3, ': ', flags2Str(part.flags):11, sanitizeName(part.name):32, part.startBlock:10, part.blocks:10); if PartBoot in part.flags then write(' ', part.bootBlocks); writeln; lastPartNo := totalPartNo; end; totalPartNo := totalPartNo + 1; end; end; writeln('Flags: P=Physical B=Boot E=Enabled L=Last D=Default'); end; function askPartNo:integer; var i:integer; s:string; begin askPartNo := -1; write('Enter partition number (0-', lastPartNo, ')> '); readln(s); if length(s) > 0 then begin val(s,askPartNo,i); if i > 0 then writeln('Invalid partition number'); end; end; function askConfirmPartNo:integer; var partNo:integer; part:Partition; answer:char; begin askConfirmPartNo := -1; partNo := askPartNo; if partNo >= 0 then begin part := getPartition(partNo); write('Any data on partition ', partNo, ' (', sanitizeName(part.name), ') ', 'will be destroyed. Sure [y/n]? '); readln(answer); if upcase(answer) = 'Y' then askConfirmPartNo := partNo; end; end; function guessExtentSize(blocks:integer):integer; begin if blocks >= 4194304 then (* 2 GB *) guessExtentSize := 1048576 (* use 1MB extents *) else if blocks >= 1048576 then (* 512 MB *) guessExtentSize := 524288 (* use 512K extents *) else if blocks >= 524288 then (* 256 MB *) guessExtentSize := 131072 else if blocks >= 262144 then (* 128 MB *) guessExtentSize := 65536 else if blocks >= 32768 then (* 16 MB *) guessExtentSize := 16384 else guessExtentSize := 8192; end; function getDirSize(extentSize,blocks:integer):integer; begin getDirSize := blocks div (extentSize div 512); end; procedure createFilesystem(partNo:integer); forward; procedure addVolume; var nextFreeBlock:integer; nextBlock:integer; extentSize:integer; size:integer; i:integer; part:Partition; maxBlocks:integer; freeBlocks:integer; newPartNo:integer; newPart:Partition; begin part := getPartition(0); maxBlocks := part.blocks; nextFreeBlock := 0; (* read all partitions *) for i := 1 to lastPartNo do begin part := getPartition(i); nextBlock := part.startBlock + part.blocks; if nextBlock > nextFreeBlock then nextFreeBlock := nextBlock; end; (* remember last used block *) writeln('next free partition: ', lastPartNo + 1, ' next free block: ', nextFreeBlock); freeBlocks := maxBlocks - nextFreeBlock; if freeBlocks < 1 then writeln('Cannot add partition - no free blocks after last partition.') else begin newPartNo := lastPartNo + 1; (* remove last partition flag on previous last partition *) part.flags := part.flags - [PartLast]; putPartition(part, lastPartNo); (* create new partition *) size := freeBlocks; changeNumber('Size (blocks)', size); write('Name> ':30); readln(newPart.name); newPart.startBlock := nextFreeBlock; newPart.blocks := size; newPart.extentSize := guessExtentSize(size); newPart.dirSize := getDirSize(newPart.extentSize, size); newPart.bootBlocks := 0; (* mark new partition as last partition *) newPart.flags := [ PartEnabled, PartLast ]; putPartition(newPart, newPartNo); writeln('Partition ', newPartNo, ' created, extent size:', newPart.extentSize, ' directory size: ', newPart.dirSize); createFilesystem(newPartNo); lastPartNo := lastPartNo + 1; end; end; procedure renameVolume; var partNo:integer; newName:string; part:Partition; begin partNo := askPartNo; if partNo >= 0 then begin part := getPartition(partNo); writeln('Old partition/volume name: ', sanitizeName(part.name)); write('New partion/volume name: '); readln(newName); if length(newName) > 0 then begin part.name := newName; putPartition(part, partNo); end; end; end; procedure toggleDefaultFlag; var partNo:integer; part:Partition; begin partNo := askPartNo; if partNo >= 0 then begin part := getPartition(partNo); write('Default flag '); if PartDefault in part.flags then begin part.flags := part.flags - [PartDefault]; write('cleared'); end else begin part.flags := part.flags + [PartDefault]; write('set'); end; writeln(' on partition ', partNo, ' (', sanitizeName(part.name), ').'); putPartition(part, partNo); end; end; procedure deleteVolume; var partNo:integer; part:Partition; begin partNo := askConfirmPartNo; if partNo >= 0 then begin part := getPartition(partNo); part.flags := []; part.name := ''; part.startBlock := 0; part.blocks := 0; part.extentSize := 0; part.dirSize := 0; part.bootBlocks := 0; putPartition(part, partNo); writeln('Partition ', partNo, ' deleted.'); (* try to fix last partition flag *) (* only works if the previous entry has a valid partition *) if partNo = lastPartNo then begin lastPartNo := lastPartNo - 1; part := getPartition(lastPartNo); part.flags := part.flags + [PartLast]; putPartition(part, lastPartNo); end; end; end; procedure validatePartTable; var partNo:integer; phys:Partition; part,part2:Partition; answer:char; p,p2:integer; valid:boolean; begin valid := true; phys := getPartition(0); if not (PartPhysical in phys.flags) then begin writeln('PHYS partition missing, initialize card first!'); exit; end; if phys.blocks <> detectedCardSize then begin write('PHYS partition size does not match detected card size, fix? [y/n]'); readln(answer); if upcase(answer) = 'Y' then begin phys.blocks := detectedCardSize; putPartition(phys,0); end else valid := false; end; for p := 1 to lastPartNo do begin part := getPartition(p); if (part.startBlock < 0) or (part.startBlock + part.blocks > phys.blocks) then begin writeln('Partition ', p, ' outside of physical bounds.'); valid := false; end; if PartEnabled in part.flags then if part.dirSize <> getDirSize(part.extentSize, part.blocks) then begin write('Partition ', p, ' has an invalid directory size (is '); writeln(part.dirSize, ', should be ', getDirSize(part.extentSize, part.blocks), ').'); valid := false; end; for p2 := 1 to lastPartNo do begin part2 := getPartition(p2); if (p <> p2) then begin if ((part.startBlock >= part2.startBlock) and (part.startBlock < part2.startBlock + part2.blocks)) or ((part2.startBlock > part.startBlock) and (part2.startBlock < part.startBlock + part.blocks)) then begin writeln('Partition ',p ,' overlaps with partition ', p2); valid := false; end; if (part.name = part2.name) and (p > p2) then begin writeln('Duplicate volume name ', part.name); valid := false; end; end; end; end; write('Partition table is '); if not valid then write('in'); writeln('valid.'); end; procedure checkNewCard; forward; procedure initializeCard; var part:Partition; answer:char; p:integer; begin writeln('Initializing a card will create an empty partition table with'); writeln('the standard PHYS and BOOT partitions.'); write('This will likely destroy any data on the card - sure? [y/n] '); readln(answer); if upcase(answer) <> 'Y' then exit; (* create PHYS partition using detectedcardblocks *) part.name := 'PHYS'; part.startBlock := 0; part.blocks := detectedCardSize; part.flags := [PartPhysical]; part.extentSize := 0; part.dirSize := 0; part.bootBlocks := 0; putPartition(part,0); (* create BOOT partition without PartBoot flag *) part.name := 'BOOT'; part.startBlock := 16; (* 16 possible partition blocks with 8 partitions each *) part.blocks := 8192 - 16; (* align first volume to 4MB *) part.flags := [PartBoot, PartLast]; putPartition(part,1); part.name := ''; part.startBlock := 0; part.blocks := 0; part.flags := []; for p := 2 to 7 do putPartition(part,p); writeln('Empty partition table created.'); end; procedure createFilesystem(partNo:integer); var firstDirBlock:integer; slot:DirectorySlot; dirblk:DirBlock; dirblockCount:integer; metaSlotsCount:integer; dirSlotsPerBlock:integer; dirSlotsPerExtent:integer; part:Partition; ts:Timestamp; i,b:integer; error,devid:integer; begin devid := 0; ts := 0; part := getPartition(partNo); firstDirBlock := part.startBlock; dirSlotsPerBlock := 512 div 64; dirSlotsPerExtent := part.extentSize div 64; dirblockCount := (part.dirSize - 1) div dirSlotsPerBlock + 1; metaSlotsCount := (part.dirSize - 1) div dirSlotsPerExtent + 1; writeln('partition size: ', part.blocks); writeln('extent size: ', part.extentSize); writeln('directory size: ', part.dirSize); { writeln('dirslots per extent:', dirSlotsPerExtent); writeln('dirblocks: ', dirblockCount); writeln('metaslots: ', metaSlotsCount); writeln('first dir block: ', firstDirBlock); } for b := firstDirBlock to firstDirBlock + dirblockCount - 1 do begin for i := 0 to dirSlotsPerBlock - 1 do begin if metaSlotsCount > 0 then begin (* write DIR/Reserved directory slots *) slot.name := 'DIR'; slot.flags := [ SlotReserved ]; metaSlotsCount := metaSlotsCount - 1; end else begin (* write Free + EndScan directory slots *) slot.name := ''; slot.flags := [ SlotFree , SlotEndScan ]; end; slot.sizeBytes := 0; slot.createTime := ts; slot.modTime := ts; slot.generation := 0; slot.owner := 0; dirBlk[i] := slot; end; writedirblk(b, dirBlk, error, devid); if error > 0 then writeln('error writing block ', b, ': ', error); end; writeln('Volume ', part.name, ' initialized.'); end; procedure initializeVolume; var partNo:integer; part:Partition; begin partNo := askConfirmPartNo; if partNo >= 0 then begin part := getPartition(partNo); if not (PartEnabled in part.flags) then writeln('Wrong partition flags (must be Enabled)') else createFilesystem(partNo); end; end; procedure rawEdit; var partNo:integer; newName:string; part:Partition; buf:string; begin writeln('Raw editing partition entry - use caution!'); partNo := askPartNo; if partNo >= 0 then begin part := getPartition(partNo); writeln('Volume name: ', sanitizeName(part.name)); write('Flags> ':30); readln(buf); if length(buf) > 0 then part.flags := str2flags(buf); changeNumber('Start block', part.startBlock); changeNumber('Size (blocks)', part.blocks); changeNumber('Extent size (blocks)', part.extentSize); changeNumber('Dir size (slots)', part.dirSize); changeNumber('Boot blocks', part.bootBlocks); putPartition(part, partNo); end; end; procedure installBoot; var bootfile:file; name:string; part:Partition; partNo:integer; buf:IOBlock; b,blkCount:integer; devId:integer; error:integer; procedure readWordsIntoBuf; var i:integer; w:integer; c1,c2,c3,c4:char; begin for i := 0 to 127 do begin if not eof(bootfile) then begin read(bootfile, c1, c2, c3, c4); w := (ord(c1) shl 24) or (ord(c2) shl 16) or (ord(c3) shl 8) or ord(c4); end else w := 0; buf[i] := w; end; end; begin devId := 0; (* only one device supported *) partNo := 1; (* BOOT partition is always at position 1 *) part := getPartition(partNo); if part.name <> 'BOOT' then begin writeln('No BOOT partition at position 1.'); exit; end; write('Boot file name> '); readln(name); if length(name) > 0 then begin open(bootfile, name, ModeReadonly); if IOResult(bootfile) <> 0 then writeln('Error opening file: ', ErrorStr(IOResult(bootfile))) else begin blkCount := filesize(bootfile) div 512 + 1; if blkCount > part.blocks then writeln('Boot partition too small, need ', blkCount) else begin part.bootBlocks := blkCount; if not (PartBoot in part.flags) then begin write('Boot flag set'); writeln(' on partition ', partNo, ' (', sanitizeName(part.name), ').'); part.flags := part.flags + [PartBoot]; end; putPartition(part, partNo); for b := 0 to blkCount - 1 do begin readWordsIntoBuf; writeblock(part.startBlock + b, buf, error, devId); if error <> 0 then writeln('Error in writeblock ', b, ': ', error); end; writeln(blkCount, ' boot blocks written.'); end; close(bootfile); end; end; end; procedure showMenu; begin writeln; writeln('L)ist partitions V)alidate partition table T)oggle default volume flag'); writeln('A)dd volume R)ename volume D)elete volume I)nitialize volume'); writeln('Read N)ew card Initialize C)ard'); writeln('E)dit partition Install B)oot file eX)it'); write('> '); end; procedure invalidCommand; begin writeln('Invalid command.'); end; procedure command(cmd:char; var done:boolean); begin case cmd of 'L': printPartTable; 'A': addVolume; 'R': renameVolume; 'D': deleteVolume; 'V': validatePartTable; 'T': toggleDefaultFlag; 'I': initializeVolume; 'N': checkNewCard; 'C': initializeCard; 'B': installBoot; 'E': rawEdit; 'X',#24: done := true; else invalidCommand; end; end; function changesPending:boolean; var i:integer; begin changesPending := false; for i := 0 to LastPartBlock do if changed[i] then changesPending := true; end; procedure checkNewCard; begin if changesPending then writeln('WARNING: Discarding partition table changes.'); initDevices; detectedCardSize := cardsize; writeln('Detected card size: ', detectedCardSize); readPartTable; printPartTable; end; begin checkNewCard; repeat showMenu; read(cmd); writeln; command(Upcase(cmd), done); until done; writePartitions; end.