#!/usr/bin/perl
#
# mkfs7: Make a PDP-7 filesystem image for SimH
#
# (c) 2016 Warren Toomey, GPL3
#
use strict;
use warnings;
use Data::Dumper;

# Constants
use constant NUMBLOCKS => 8000;		# Number of blocks on a surface
use constant WORDSPERBLK => 64;		# 64 words per block
use constant NUMINODEBLKS => 710;	# Blocks 1 to 710 for i-nodes
use constant FIRSTINODEBLK => 1;	# First i-node block number
use constant INODESIZE => 12;		# Size of an i-node
use constant INODESPERBLK => WORDSPERBLK/INODESIZE;
use constant DIRENTSIZE => 8;		# Size of an directory entry
use constant DIRENTSPERBLK => WORDSPERBLK/DIRENTSIZE;

use constant ROOT_UID => -1;		# Native root user-id
use constant MAXINT    => 0777777;      # Biggest unsigned integer

use constant DD_INUM    => 2;		# I-number of the dd directory

# 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_OWNERREAD =>  0000010;
use constant I_OWNERWRITE => 0000004;
use constant I_WORLDREAD =>  0000010;
use constant I_WORLDWRITE => 0000004;

# 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=0;
my @Block;			# Array of blocks and words in each block
my $nextblknum= NUMINODEBLKS+1;	# Next free block number
my $nextinum= 1;		# i-num 0 is never used
my @Dirstack;			# Stack of directories. Each value is a ref
				# to a [ blocknum, offset, inum ] array which
				# is the next free position in the directory

# Debug printing
sub dprint {
  print(@_) if ($debug);
}
sub dprintf {
  printf(@_) if ($debug);
}


# Given a size in words, allocate and return a set of block numbers
# for the entity
sub allocate_blocks {
  my $numwords= shift;
  my @blklist;

  my $numblocks= int( ($numwords+WORDSPERBLK-1) / WORDSPERBLK );
  die("Not enough blocks\n") if (($nextblknum+$numblocks) >= NUMBLOCKS);
  foreach my $b (1 .. $numblocks) {
    push(@blklist, $nextblknum++);
  }
  dprint("Allocated blocks for size $numwords: $blklist[0] .. $blklist[-1]\n");
  return(@blklist);
}

# Allocate and return either the specified i-node or the next
# available one if there is no argument
sub allocate_inode {
  my $inum= shift;
  return($nextinum++) if (!defined($inum));
  die("i-num $inum already allocated\n") if ($inum< $nextinum);
  $nextinum= $inum+1;
  return($inum);
}

# Given a list of block numbers, allocate a set of indirect blocks
# and install block pointers into the indirect blocks. Return the
# list of indirect block numbers.
sub build_indirect_blocks {
  my @blklist= @_;
  my $blkcount= @blklist;

  # Divide the number of data blocks by WORDSPERBLK and round up, so
  # we know how many indirect blocks to allocate.
  my $indcount= int(($blkcount+WORDSPERBLK-1)/WORDSPERBLK);

  # Get enough indirect blocks
  my @indlist= allocate_blocks($indcount);

  # Now fill in the pointers
  my $indblock= $indlist[0];
  my $offset= 0;
  foreach my $datablock (@blklist) {
    $Block[$indblock][$offset++]= $datablock;
    if ($offset == WORDSPERBLK) {
      $offset=0; $indblock++;
    }
  }

  # Return the indirect block numbers
  dprint("Built indirect blocks $indlist[0] .. $indlist[-1]\n");
  return(@indlist);
}

# 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);
  dprint("inum $inum => block $blocknum offset $offset\n");
  return($blocknum, $offset);
}

# Given an i-node number (possibly undef), permission, filetype, uid, size
# and up to seven direct or indirect block numbers, fill in the given i-node
# with the data. If the i-node number is undef, allocate an i-node number.
# Return the i-node number used.
sub fill_inode {
  my ($inum, $perms, $filetype, $uid, $size, @blklist)= @_;
  die("Too many blocks\n") if (@blklist>7);

  # Deal with the root user-id
  $uid= MAXINT if ($uid==ROOT_UID);

  # Calculate the block number and word offset for this
  $inum= allocate_inode() if (!defined($inum));
  my ($blocknum, $offset)= get_inode_block_offset($inum);

  # Fill in the easy fields
  $Block[$blocknum][$offset+I_UID]= $uid;
  $Block[$blocknum][$offset+I_SIZE]= $size;
  $Block[$blocknum][$offset+I_NLKS]= 1;

  my $i= $offset;
  foreach my $datablocknum (@blklist) {
    $Block[$blocknum][$i+I_DISKPS]= $datablocknum;
  }

  # Deal with the flags and see if it's a large file
  my $flags = $perms | $filetype | I_USED;
  $flags |= I_LARGE if ($size> WORDSPERBLK * I_NUMBLKS);
  $Block[$blocknum][$offset+I_FLAGS]= $flags;
  
  dprintf("fill inum %d: flags %06o uid %06o size %d\n", $inum, $flags, $uid, $size);
  return($inum);
}

# Convert an ASCII string into an array of 18-bit word values
# where two characters are packed into each word. Put NUL in
# if the string has an odd number of characters. Return the array
sub ascii2words {
    my $str = shift;
    my @words;
    for ( my $i = 0 ; $i < length($str) ; $i += 2 ) {
        my $c1 = substr( $str, $i,     1 ) || "\0";
        my $c2 = substr( $str, $i + 1, 1 ) || "\0";

        push( @words, ( ord($c1) << 9 ) | ord($c2) );
    }
    return (@words);
}

# Add an extra block to an i-node. NOTE: for now, we don't change the size
# in the i-node.
sub add_block_to_inode {
  my ($blknum, $inum) = @_;

  my ($iblock, $offset)= get_inode_block_offset($inum);

  foreach my $i (1 .. I_NUMBLKS) {
    next if ($Block[$iblock][$offset+$i]!=0);	# Skip in-use blocks
    $Block[$iblock][$offset+$i]= $blknum;
    return;
  }
  die("Unable to add extra block to i-node $inum\n");
  dprint("Added block $blknum to i-node $inum\n");
}

# Add a name and an i-node number to the current directory in the
# directory stack.
sub add_direntry {
  my ($name, $inum)= @_;
  dprint("Adding $name inode $inum to current directory\n");

  # Get the block and offset to the next empty slot in the directory
  my $dirref= $Dirstack[-1];

  if (!defined($dirref)) {
    print("Empty dirstack, we must be building the root dir\n");
    return;
  }

  my $blocknum= $dirref->[0];
  my $offset= $dirref->[1];

  # Convert the name into four words
  my @wlist= ascii2words($name);

  # Fill in the directory entry
  $Block[$blocknum][$offset+D_INUM]= $inum;
  $Block[$blocknum][$offset+D_NAME]= pop(@wlist);
  $Block[$blocknum][$offset+D_NAME+1]= pop(@wlist);
  $Block[$blocknum][$offset+D_NAME+2]= pop(@wlist);
  $Block[$blocknum][$offset+D_NAME+3]= pop(@wlist);

  # Move up to the next position in the directory.
  $dirref->[1] += D_NUMWORDS;

  # If we have filled the directory up, allocate another block to it
  if ($dirref->[1] == WORDSPERBLK) {
    my $nextblock= allocate_blocks(WORDSPERBLK);
    $dirref->[0]= $nextblock;
    $dirref->[1]= 0;

    # And add this new block to the directory's i-node
    add_block_to_inode($nextblock, $dirref->[2]);
  }
}

# Given a name, perms, a user-id and an optional i-node number, make a
# directory. Link it to the previous directory in the directory stack.
# Allocate blocks and i-nodes for it. Add a "dd" entry as well.
sub make_dir {
  my ($dirname, $perms, $uid, $inum)= @_;

  # Get an i-node number or validate the one we got
  $inum= allocate_inode($inum);

  # Get a block for this directory
  my $dirblock= allocate_blocks(WORDSPERBLK);

  # Add this to the previous directory
  # and fill the i-node with the details
  add_direntry($dirname, $inum);
  fill_inode($inum, $perms, I_DIRECTORY, $uid, 0, $dirblock);

  # Make this the top directory on the dirstack
  dprint("Pushing dir block $dirblock inum $inum to dirstack\n");
  push(@Dirstack, [$dirblock, 0, $inum]);

  # Add a "dd" entry to this directory
  add_direntry("dd", DD_INUM);
  dprintf("Made directory %s perms %06o uid %d in i-node %d\n\n",
					$dirname, $perms, $uid, $inum);
}

# Read a word from a file in paper tape binary format.
# Return -1 on EOF
sub read_word {
    my $FH = shift;
    # Convert three bytes into one 18-bit word
    return(-1) if ( read( $FH, my $three, 3 ) != 3 ); # Not enough bytes read
    my ( $b1, $b2, $b3 ) = unpack( "CCC", $three );
    return ((($b1 & 077) << 12 ) |
            (($b2 & 077) << 6 ) |
             ($b3 & 077));
}

# Given a filename, perms, user-id and an external file, add a file to the
# filesystem. Add an entry to this file in the current directory on
# the dirstack.
sub add_file {
  my ($name, $perms, $uid, $extfile) = @_;
  dprintf("Adding file %s perms %06o uid %d extfile %s\n",
	$name, $perms, $uid, $extfile);

  # Open the external file
  open(my $IN, "<", $extfile) || die("Can't open $extfile: $!\n");

  # Determine if this is ASCII or binary
  my $isbinary=0;
  my $c = getc($IN);
  seek($IN, 0, 0);
  $isbinary=1 if ((ord($c) & 0300) == 0200);

  # Read the whole file into a buffer, converting from ASCII or sixbit encoding
  my @buf;
  my $size;

  while (1) {
        if ( $isbinary ) {
            # Convert three bytes into one 18-bit word
            my $result = read_word($IN);
            last if ($result == -1);
            $buf[$size++] = $result;
        }
        else {
            # Convert two ASCII characters into one 18-bit word
            my $c1 = getc($IN);
            last if ( !defined($c1) );    # No character, leave the loop
            my $word = ord($c1) << 9;
            my $c2 = getc($IN);
            $word |= ord($c2) if (defined($c2));
            $buf[$size++] = $word;
        }
  }

  # Allocate enough blocks for the file
  my @blklist= allocate_blocks($size);

  # If it's too big, allocate indirect blocks
  my $large=0;
  my @indblocks;
  if (@blklist > I_NUMBLKS) {
    $large=1;
    @indblocks= build_indirect_blocks(@blklist);
  }

  # Allocate and fill in the i-node
  my $inum= allocate_inode();
  if ($large) {
    fill_inode($inum, $perms, 0, $uid, $size, @indblocks);
  } else {
    fill_inode($inum, $perms, 0, $uid, $size, @blklist);
  }

  # and add the entry in the directory
  add_direntry($name, $inum);
  dprint("Done adding file $name as inum $inum\n\n");
}

# Given a name, perms, uid and i-number
# add a special file to the filesystem
sub add_special {
  my ($name, $perms, $uid, $inum) = @_;

  # Allocate and fill in the i-node
  my $inum= allocate_inode($inum);
  fill_inode($inum, $perms, I_SPECIAL, $uid, 0);

  # Add the entry in the directory
  add_direntry($name, $inum);
  dprint("Done adding special file $name inum $inum\n\n");
}

### MAIN PROGRAM

$debug=1;
make_dir("dd", 014, -1, 2);
make_dir("system", 014, -1, 3);
add_file("init", 014, -1, "proto");
pop(@Dirstack);
add_special("ttyin", 014, -1, 6);
add_special("keyboard", 014, -1, 7);
add_special("pptin", 014, -1, 8);
add_special("ttyout", 014, -1, 10);
add_special("display", 014, -1, 11);
add_special("pptout", 014, -1, 12);
add_file("as", 014, -1, "b.c");
add_file("a7out", 014, -1, "a7out");
add_file("oflow", 014, -1, "a7out");
