mirror of
https://github.com/livingcomputermuseum/pdp7-unix.git
synced 2026-01-12 00:02:47 +00:00
340 lines
10 KiB
Perl
Executable File
340 lines
10 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
#
|
|
# fsck7: Check a PDP-7 filesystem for consistency
|
|
#
|
|
# (c) 2016 Warren Toomey, GPL3
|
|
#
|
|
use strict;
|
|
use warnings;
|
|
use Fcntl qw(:flock SEEK_SET);
|
|
use Data::Dumper;
|
|
use Getopt::Long qw(GetOptions);
|
|
|
|
Getopt::Long::Configure qw(gnu_getopt);
|
|
|
|
# Constants
|
|
use constant NUMBLOCKS => 8000; # Number of blocks on a surface
|
|
use constant WORDSPERBLK => 64; # 64 words per block
|
|
use constant LASTFREEBLOCK => 6399; # That's what s9.s uses
|
|
use constant NUMINODEBLKS => 710; # Number of i-node blocks
|
|
use constant FIRSTINODEBLK => 2; # First i-node block number
|
|
use constant INODESIZE => 12; # Size of an i-node
|
|
use constant INODESPERBLK => int( WORDSPERBLK / INODESIZE );
|
|
use constant DIRENTSIZE => 8; # Size of an directory entry
|
|
use constant DIRENTSPERBLK => WORDSPERBLK / DIRENTSIZE;
|
|
|
|
use constant MAXINT => 0777777; # Biggest unsigned integer
|
|
use constant SIGN => 0400000; # Sign bit
|
|
use constant BYTESPERWORD => 4; # We encode each word into 4 bytes
|
|
|
|
# i-node field offsets
|
|
use constant I_FLAGS => 0;
|
|
use constant I_DISKPS => 1;
|
|
use constant I_UID => 8;
|
|
use constant I_NLKS => 9;
|
|
use constant I_SIZE => 10;
|
|
use constant I_UNIQ => 11;
|
|
|
|
use constant I_NUMBLKS => 7; # Seven block pointers in i-node
|
|
|
|
# i-node flag masks
|
|
use constant I_USED => 0400000;
|
|
use constant I_LARGE => 0200000;
|
|
use constant I_SPECIAL => 0000040;
|
|
use constant I_DIRECTORY => 0000020;
|
|
use constant I_FILE => 0000000;
|
|
use constant I_OWNERREAD => 0000010;
|
|
use constant I_OWNERWRITE => 0000004;
|
|
use constant I_WORLDREAD => 0000002;
|
|
use constant I_WORLDWRITE => 0000001;
|
|
|
|
use constant I_LINK => 0000001; # Never used in an i-node: just internal use
|
|
|
|
# Directory field offsets
|
|
use constant D_INUM => 0;
|
|
use constant D_NAME => 1;
|
|
use constant D_UNIQ => 5;
|
|
use constant D_NUMWORDS => 8; # Eight words in a direntry
|
|
|
|
# Globals
|
|
my ($debug, $no_dd) = (0,0);
|
|
my @Block; # Array of blocks and words in each block
|
|
my @Freelist; # List of free block numbers
|
|
my @Usedlist; # List of in-use block numbers
|
|
my @Usedinode; # List of i-nodes in use
|
|
my @Dirdone; # List of directory i-nodes already processed
|
|
|
|
# Debug printing
|
|
sub dprint {
|
|
print(@_) if ($debug);
|
|
}
|
|
|
|
sub dprintf {
|
|
printf(@_) if ($debug);
|
|
}
|
|
|
|
### read a word from a file in SimH format
|
|
### return -1 on EOF
|
|
sub read_word {
|
|
my $F = shift;
|
|
|
|
# Convert four bytes into one 18-bit word
|
|
return -1 if ( read( $F, my $four, 4 ) != 4 ); # Not enough bytes read
|
|
my ( $b1, $b2, $b3, $b4 ) = unpack( "CCCC", $four );
|
|
return (
|
|
( $b1 & 0xff ) | ( ( $b2 & 0xff ) << 8 ) | ( ( $b3 & 0xff ) << 16 ) |
|
|
( ( $b4 & 0xff ) << 24 ) );
|
|
}
|
|
|
|
# Return blocknumber and offset for a specific i-node
|
|
sub get_inode_block_offset {
|
|
my $inum = shift;
|
|
my $blocknum = FIRSTINODEBLK + int( $inum / INODESPERBLK );
|
|
my $offset = INODESIZE * ( $inum % INODESPERBLK );
|
|
return ( $blocknum, $offset );
|
|
}
|
|
|
|
# Mark a block as being in-use
|
|
sub mark_block_inuse {
|
|
my $usedblk = shift;
|
|
die("bad usedblk $usedblk\n") if ($usedblk >= NUMBLOCKS);
|
|
|
|
dprint("Block $usedblk is in-use\n");
|
|
print("Free block $usedblk is being used\n")
|
|
if ( $Freelist[$usedblk] );
|
|
print("In-use block $usedblk is being re-used\n")
|
|
if ( $Usedlist[$usedblk] );
|
|
$Usedlist[$usedblk] = 1;
|
|
}
|
|
|
|
# Given an i-node number and a flag mask, return true
|
|
# if the flags contains that mask, 0 otherwise
|
|
sub is_inode {
|
|
my ( $inode, $mask ) = @_;
|
|
my ( $blocknum, $offset ) = get_inode_block_offset($inode);
|
|
my $flags = $Block[$blocknum][ $offset + I_FLAGS ];
|
|
return ( $flags & $mask );
|
|
}
|
|
|
|
# Given an i-node number, return the flags, uid, # link,
|
|
# file size, uniq number and list of disk blocks for
|
|
# the i-node (if $wantblocks is true).
|
|
# Return undef if the i-node is not used.
|
|
sub get_inode {
|
|
my ( $inode, $wantblocks ) = @_;
|
|
my ( $blocknum, $offset ) = get_inode_block_offset($inode);
|
|
my $flags = $Block[$blocknum][ $offset + I_FLAGS ];
|
|
my $uid = $Block[$blocknum][ $offset + I_UID ];
|
|
my $links = $Block[$blocknum][ $offset + I_NLKS ];
|
|
my $size = $Block[$blocknum][ $offset + I_SIZE ];
|
|
my $uniq = $Block[$blocknum][ $offset + I_UNIQ ];
|
|
|
|
# Skip unused i-nodes
|
|
return (undef) if ( ( $flags & I_USED ) == 0 );
|
|
|
|
# Return now if they don't want the blocks
|
|
return ( $flags, $uid, $links, $size, $uniq ) if ( !$wantblocks );
|
|
|
|
# Build a list of in-use blocks
|
|
my @used;
|
|
if ( $flags & I_LARGE ) {
|
|
|
|
# Large file
|
|
foreach
|
|
my $o ( $offset + I_DISKPS .. $offset + I_DISKPS + I_NUMBLKS - 1 )
|
|
{
|
|
my $usedptr = $Block[$blocknum][$o];
|
|
next if ( $usedptr == 0 );
|
|
die("bad usedptr $usedptr in i-node $inode\n") if ($usedptr >= NUMBLOCKS);
|
|
|
|
mark_block_inuse($usedptr);
|
|
foreach my $indo ( 0 .. WORDSPERBLK - 1 ) {
|
|
|
|
my $usedblk = $Block[$usedptr][$indo];
|
|
next if ( $usedblk == 0 );
|
|
push( @used, $usedblk );
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Small file
|
|
foreach
|
|
my $o ( $offset + I_DISKPS .. $offset + I_DISKPS + I_NUMBLKS - 1 )
|
|
{
|
|
my $usedblk = $Block[$blocknum][$o];
|
|
next if ( $usedblk == 0 );
|
|
push( @used, $usedblk );
|
|
}
|
|
}
|
|
return ( $flags, $uid, $links, $size, $uniq, @used );
|
|
}
|
|
|
|
# Given the address of a four word argument string in
|
|
# a block, return a copy of the string in ASCII format.
|
|
# Lose any trailing spaces as well.
|
|
sub word2filename {
|
|
my ( $block, $offset ) = @_;
|
|
my $result = "";
|
|
|
|
foreach ( 1 .. 4 ) {
|
|
|
|
my $word = $Block[$block][ $offset++ ];
|
|
my $c1 = ( $word >> 9 ) & 0177;
|
|
my $c2 = $word & 0177;
|
|
$result .= chr($c1) . chr($c2);
|
|
}
|
|
$result =~ s{ *$}{};
|
|
return ($result);
|
|
}
|
|
|
|
# Print out a directory entry
|
|
sub print_direntry {
|
|
my ( $inum, $name ) = @_;
|
|
my ( $flags, $uid, $links, $size, $uniq ) = get_inode( $inum, 0 );
|
|
if ( !defined($flags) ) {
|
|
printf( "%4d unallocated i-node in this dir named %s\n", $inum, $name );
|
|
return;
|
|
}
|
|
|
|
# Get the text perm characters
|
|
my ( $p1, $p2, $p3, $p4, $p5 ) = ( 's', '-', '-', '-', '-' );
|
|
$p1 = 'l' if ( $flags & I_LARGE );
|
|
$p1 = 'i' if ( $flags & I_SPECIAL );
|
|
$p1 = 'd' if ( $flags & I_DIRECTORY );
|
|
$p2 = 'r' if ( $flags & I_OWNERREAD );
|
|
$p3 = 'w' if ( $flags & I_OWNERWRITE );
|
|
$p4 = 'r' if ( $flags & I_WORLDREAD );
|
|
$p5 = 'w' if ( $flags & I_WORLDWRITE );
|
|
|
|
# Convert links and uid
|
|
$links = MAXINT - $links + 1;
|
|
$uid = -1 if ( $uid == MAXINT );
|
|
|
|
# Warn about empty filename
|
|
$name= "EMPTY FILENAME" if ($name=~ m{/$});
|
|
|
|
printf( "%4d %s%s%s%s%s %2d %3o %5d %s\n",
|
|
$inum, $p1, $p2, $p3, $p4, $p5, $links, $uid & 0777, $size, $name );
|
|
}
|
|
|
|
# Given a directory's i-num and name, check it
|
|
sub check_directory {
|
|
my ( $inode, $name ) = @_;
|
|
my @dirtocheck; # List of dirs to check
|
|
|
|
print("Directory $name, i-node $inode\n");
|
|
|
|
my ( $flags, $uid, $links, $size, $uniq, @used ) = get_inode( $inode, 1 );
|
|
if ( !defined($flags) ) {
|
|
print("Directory $name has empty i-node $inode\n");
|
|
return;
|
|
}
|
|
|
|
if ( ( $flags & I_DIRECTORY ) == 0 ) {
|
|
print("Directory $name i-node $inode not a directory\n");
|
|
return;
|
|
}
|
|
$Dirdone[$inode] = 1;
|
|
|
|
# Read the contents of the data blocks
|
|
foreach my $block (@used) {
|
|
for (
|
|
my $direntoff = 0 ;
|
|
$direntoff < WORDSPERBLK ;
|
|
$direntoff += DIRENTSIZE
|
|
)
|
|
{
|
|
my $inum = $Block[$block][ $direntoff + D_INUM ];
|
|
my $direntname = word2filename( $block, $direntoff + D_NAME );
|
|
next if ( $inum == 0 );
|
|
print_direntry( $inum, "$name/$direntname" );
|
|
|
|
# Check sub-directories
|
|
if ( is_inode( $inum, I_DIRECTORY ) && ( !$Dirdone[$inum] ) ) {
|
|
push( @dirtocheck, [ $inum, "$name/$direntname" ] );
|
|
}
|
|
}
|
|
}
|
|
print("\n");
|
|
|
|
# Now check the subdirs
|
|
foreach my $d (@dirtocheck) {
|
|
check_directory( $d->[0], $d->[1] );
|
|
}
|
|
|
|
}
|
|
|
|
# Keep this near the GetOptions call to make it easy to add documentation!
|
|
sub usage {
|
|
die("Usage: $0 [--debug] imagefile\n");
|
|
}
|
|
|
|
### MAIN PROGRAM
|
|
|
|
GetOptions(
|
|
'debug|d' => \$debug,
|
|
'no_dd|3' => \$no_dd,
|
|
)
|
|
or usage();
|
|
|
|
usage() if ( @ARGV < 1 );
|
|
|
|
# Open the image and skip the first surface
|
|
open( my $IN, "<", $ARGV[0] ) || die("Can't open $ARGV[0]: $!\n");
|
|
seek($IN, NUMBLOCKS*WORDSPERBLK*BYTESPERWORD, SEEK_SET) ||
|
|
die("Cannot seek: $!\n");
|
|
|
|
# Now read the filesystem into the Block array
|
|
foreach my $block ( 0 .. NUMBLOCKS - 1 ) {
|
|
foreach my $posn ( 0 .. WORDSPERBLK - 1 ) {
|
|
my $word = read_word($IN);
|
|
last if ( $word == -1 );
|
|
$Block[$block][$posn] = $word;
|
|
}
|
|
}
|
|
|
|
# Build and check the free list
|
|
my $freeptr = $Block[0][0];
|
|
while ( $freeptr != 0 ) {
|
|
|
|
# Add nine blocks to the free list
|
|
foreach my $posn ( 1 .. 9 ) {
|
|
my $freeblock = $Block[$freeptr][$posn];
|
|
if ( $freeblock != 0 ) {
|
|
print("Block $freeblock multiply free\n")
|
|
if ( $Freelist[$freeblock] );
|
|
$Freelist[$freeblock] = 1;
|
|
}
|
|
}
|
|
|
|
# Move the pointer up
|
|
$freeptr = $Block[$freeptr][0];
|
|
}
|
|
|
|
# Check the list of i-nodes
|
|
foreach my $inode ( 0 .. NUMINODEBLKS * INODESPERBLK - 1 ) {
|
|
|
|
my ( $flags, $uid, $links, $size, $uniq, @used ) = get_inode( $inode, 1 );
|
|
next if ( !defined($flags) );
|
|
$Usedinode[$inode] = 1;
|
|
dprint("I-node $inode in use\n");
|
|
|
|
# Check number of links is not zero
|
|
print("I-node $inode link count 0\n") if ( $links == 0 );
|
|
|
|
# Mark all the file's blocks in-use
|
|
foreach my $b (@used) {
|
|
mark_block_inuse($b);
|
|
}
|
|
dprint("\n");
|
|
}
|
|
|
|
# Now check the directories. We start with dd at i-num 4.
|
|
if ($no_dd) {
|
|
check_directory( 2, "" );
|
|
} else {
|
|
check_directory( 4, "dd" );
|
|
}
|
|
|
|
exit(0);
|