#!/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 %3d %5d %s\n",
        $inum, $p1, $p2, $p3, $p4, $p5, $links, $uid, $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);
