#!/usr/bin/perl -w
#
# flopcop - analyze a FAT12 floppy disk image
# created with dd.  Prints relevant information
# such as the BIOS Parameter Block, root directory
# entries, offsets and sizes of all files in the
# root directory, and FAT chain image file offsets
# for each file.
#
# limitations: does not traverse sub directories,
# only analyzes root dir entries.
#
# usage: flopcop imagefile
#
# N. DeBaggis 10-9-2002
#

use strict;


my $dir_struct_len = 32;

my $buf;
my %bsect;
my %geometry;
my @files;

open(FH, "$ARGV[0]");
binmode(FH);

readboot();
printboot();
calcvalues();

print "$ARGV[0] Image file offsets:\n";
print '-' x 50 . "\n";
foreach(sort keys %geometry){
   printf("%-20s = 0x%08x %d\n", $_, $geometry{$_}, $geometry{$_});
}


my $rootbase = $geometry{'root_offset'};
for(my $i = 0; $i < $bsect{'root_entries'}; $i++){
   push @files, readdirent($rootbase);
   $rootbase += $dir_struct_len;
}

print "\n$ARGV[0] MSDOS 8.3 filename info:\n";

foreach(@files){
   $buf = ();
   if(($_->{'fileattr'} != 0x0f) && ($_->{'filecluster'} > 1)){
      printdirent($_);
      print "\n";
   }
}

close(FH);



#
# readboot()
#
# reads the first 512 bytes from the dd created image
# stores the BIOS Parameter Block values in the %bsect
# hash.
#
sub readboot{
   seek(FH, 0, 0);
   read(FH, $buf, 512);
   $bsect{'jmp_opcode'} = unpack('@0H6',$buf);
   $bsect{'oem_name'} = unpack('@3a8', $buf);
   $bsect{'bytes_per_sector'} = unpack('@11S1', $buf);
   $bsect{'sectors_per_cluster'} = unpack('@13C1', $buf);
   $bsect{'reserved_sectors'} = unpack('@14S1', $buf);
   $bsect{'n_fats'} = unpack('@16C1', $buf);
   $bsect{'root_entries'} = unpack('@17S1', $buf);
   $bsect{'total_sectors'} = unpack('@19S1', $buf);
   $bsect{'media_descriptor'} = unpack('@21C1', $buf);
   $bsect{'sectors_per_fat'} = unpack('@22S1', $buf);
   $bsect{'sectors_per_track'} = unpack('@24S1', $buf);
   $bsect{'n_heads'} = unpack('@26S1', $buf);
   $bsect{'hidden_sectors'} = unpack('@28L1', $buf);
   $bsect{'total_sectors_big'} = unpack('@32L1', $buf);
   $bsect{'drive_number'} = unpack('@36C1', $buf);
   $bsect{'unused_byte'} = unpack('@37C1', $buf);
   $bsect{'ext_boot_signature'} = unpack('@38C1', $buf);
   $bsect{'serial_number'} = unpack('@39L1', $buf);
   $bsect{'volume_label'} = unpack('@43a11', $buf);
   $bsect{'fat_type'} = unpack('@54a8', $buf);
   $bsect{'boot_code'} = substr($buf, 62, 448);
   $bsect{'boot_signature'} = unpack('@510S1', $buf);
}


sub printboot{
   print "$ARGV[0] BIOS Parameter Block:\n";
   print '-' x 50 . "\n";
   printf("%-20s = 0x%s\n", "jmp_opcode", $bsect{'jmp_opcode'});
   printf("%-20s = %s\n", "oem_name", $bsect{'oem_name'});
   printf("%-20s = %d\n", "bytes_per_sector", $bsect{'bytes_per_sector'});
   printf("%-20s = %d\n", "sectors_per_cluster", $bsect{'sectors_per_cluster'});
   printf("%-20s = %d\n", "reserved_sectors", $bsect{'reserved_sectors'});
   printf("%-20s = %d\n", "n_fats", $bsect{'n_fats'});
   printf("%-20s = %d\n", "root_entries", $bsect{'root_entries'});
   printf("%-20s = %d\n", "total_sectors", $bsect{'total_sectors'});
   printf("%-20s = 0x%02x\n", "media_descriptor", $bsect{'media_descriptor'});
   printf("%-20s = %d\n", "sectors_per_fat", $bsect{'sectors_per_fat'});
   printf("%-20s = %d\n", "sectors_per_track", $bsect{'sectors_per_track'});
   printf("%-20s = %d\n", "n_heads", $bsect{'n_heads'});
   printf("%-20s = %d\n", "hidden_sectors", $bsect{'hidden_sectors'});
   printf("%-20s = %d\n", "total_sectors_big", $bsect{'total_sectors_big'});
   printf("%-20s = %d\n", "drive_number", $bsect{'drive_number'});
   printf("%-20s = 0x%02x\n", "unused_byte", $bsect{'unused_byte'});
   printf("%-20s = 0x%02x\n", "ext_boot_signature", $bsect{'ext_boot_signature'});
   printf("%-20s = 0x%04x\n", "serial_number", $bsect{'serial_number'});
   printf("%-20s = %s\n", "volume_label", $bsect{'volume_label'});
   printf("%-20s = %s\n", "fat_type", $bsect{'fat_type'});
   printf("%-20s = 0x%04x\n", "boot_signature", $bsect{'boot_signature'});
   print "\n";
   print "boot_code = \n";
   print '-' x 50 . "\n";

   # from perldoc.com
   # http://www.perldoc.com/perl5.8.0/pod/perlpacktut.html
   my $i;
   print map { ++$i % 16 ? "$_ " : "$_\n" }
      unpack( 'H2' x length( $bsect{'boot_code'} ), $bsect{'boot_code'} ),
      length( $bsect{'boot_code'} ) % 16 ? "\n" : '';

   print "\n";
}


#
# calcvalues()
#
# calculate the image file offsets and values for the key
# locations of the disk image.
#
sub calcvalues{
   $geometry{'fat_size'} = $bsect{'sectors_per_fat'} * $bsect{'bytes_per_sector'};
   $geometry{'root_size'} = $bsect{'root_entries'} * $dir_struct_len;
   $geometry{'fat1_offset'} = $bsect{'bytes_per_sector'};
   $geometry{'root_offset'} = $geometry{'fat_size'} * $bsect{'n_fats'} + $geometry{'fat1_offset'};
   $geometry{'data_offset'} = $geometry{'root_size'} + $geometry{'root_offset'};
}


#
# getoffset($cluster)
#
# returns the dd created image file offset of
# the FAT cluster.
#
sub getoffset{
   my $cluster = shift;
   my $result = 0;
   $result = $cluster;
   $result -= 2;
   $result *= $bsect{'sectors_per_cluster'};
   $result += $geometry{'data_offset'} / $bsect{'bytes_per_sector'};
   $result *= $bsect{'bytes_per_sector'};
   return $result;
}


#
# followchain($cluster)
#
# follows the FAT chain from cluster
#
# returns a reference to the FAT chain array.
#
sub followchain{
   my $cluster = shift;
   my $tmp;
   my @chain;
   my $foffset;

   push @chain, $cluster;
   $foffset = $cluster + ($cluster / 2);
   $tmp = $cluster;
   seek(FH, $geometry{'fat1_offset'} + $foffset, 0);
   read(FH, $cluster, 2);
   $cluster = unpack('S1', $cluster);

   if($tmp & 1){
      $cluster >>= 4;
   }
   else{
      $cluster &= 0x0fff;
   }

   while($cluster > 0x0 && $cluster < 0xff1){
      push @chain, $cluster;
      $foffset = $cluster + ($cluster / 2);
      $tmp = $cluster;
      seek(FH, $geometry{'fat1_offset'} + $foffset, 0);
      read(FH, $cluster, 2);
      $cluster = unpack('S1', $cluster);

      if($tmp & 1){
         $cluster >>= 4;
      }
      else{
         $cluster &= 0x0fff;
      }
   }
   return \@chain;
}


#
# readdirent($offset)
#
# reads an MSDOS 8.3 directory entry from the root directory
# and stores the directory info into a hash.
#
# returns a reference to the dir entry hash.
#
sub readdirent{
   my $offset = shift;
   seek(FH, $offset, 0);
   my $tmp;
   read(FH, $tmp, $dir_struct_len);
   my %dirstruct;
   $dirstruct{'filename'} = unpack('@0a8', $tmp);
   $dirstruct{'fileext'} = unpack('@8a3', $tmp);
   $dirstruct{'fileattr'} = unpack('@11C1', $tmp);
   $dirstruct{'filereserved'} = unpack('@12C9', $tmp);
   $dirstruct{'filetime'} = unpack('@22S1', $tmp);
   $dirstruct{'filedate'} = unpack('@24S1', $tmp);
   $dirstruct{'filecluster'} = unpack('@26S1', $tmp);
   $dirstruct{'filelength'} = unpack('@28L1', $tmp);
   $dirstruct{'dirblock'} = ($offset - $geometry{'root_offset'}) / 32;
   $dirstruct{'fileslack'} = 0;
   if(my $tmp = $dirstruct{'filelength'} % ($bsect{'bytes_per_sector'} * $bsect{'sectors_per_cluster'})){
      $dirstruct{'fileslack'} = $bsect{'bytes_per_sector'} * $bsect{'sectors_per_cluster'} - $tmp;
   }
   return \%dirstruct;
}


#
# print a directory entry from a dir hash.
#
sub printdirent{
   my $hash = shift;
   my $chain;

   print '-' x 50 . "\n";

   printf("%-20s = %s",
      "filename", $hash->{'filename'});
   if($hash->{'filename'} =~ m/^\xe5/){
      print " [deleted]\n";
   }
   else{
      print "\n";
   }


   printf("%-20s = %s\n",
      "file extention", $hash->{'fileext'});

   print "\n";

   printf("%-20s - %s\n",
      "file attributes", "uuadvshr");

   printf("%-20s = %s\n",
      "", unpack('B8', pack('C1', $hash->{'fileattr'})));

   print "\n";

   printf("%-20s = 0x%04x     %s\n",
      "first cluster", $hash->{'filecluster'}, $hash->{'filecluster'});

   printf("%-20s = 0x%08x %s\n",
      "image offset", getoffset($hash->{'filecluster'}), getoffset($hash->{'filecluster'}));

   printf("%-20s = 0x%08x %s\n",
      "file length", $hash->{'filelength'}, $hash->{'filelength'});

   printf("%-20s = 0x%08x %s\n",
      "file slack", $hash->{'fileslack'}, $hash->{'fileslack'});

   print "\n";

   print "file cluster trace with image offset in hex\n[cluster:offset]:\n";
   $chain = followchain($hash->{'filecluster'});

   for(my $i = 0; $i < @$chain; $i++){
      printf("%04x:%08x", @$chain[$i], getoffset(@$chain[$i]));
      if(($i + 1) % 4){
         print "  ";
      }
      else{
         print "\n";
      }
   }
   print "\n";
}

