The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package FileSystem::LL::FAT;

#use 5.008008;
#use warnings;

require Exporter;

@ISA = qw(Exporter);

# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.

# This allows declaration	use FileSystem::LL::FAT ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
%EXPORT_TAGS = ( 'all' => [ qw(
	MBR_2_partitions debug_partitions emit_fat32 interpret_directory
	check_bootsector interpret_bootsector
	check_FAT_array FAT_2array cluster_chain read_FAT_data
	write_file write_dir list_dir compress_FAT uncompress_FAT
) ] );

@EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

@EXPORT = qw(
	
);

$VERSION = '0.05';
use strict;

my $lim_read = $ENV{FAT_READ_NEEDS_1SECTOR};
$lim_read = ($^O eq 'os2') unless defined $lim_read; #Bug in OS/2 FAT32 driver?
$lim_read = $lim_read ? 512 : (1<<24);

# Preloaded methods go here.
sub decode_fields ($$) {
  my ($fields2, $in) = (shift,shift);
  my $lastfield = @$fields2/2 - 1;
  my $extract = join ' ', @$fields2[map 2*$_ + 1, 0 .. $lastfield];
  my @values = unpack $extract, $in;
  map( ($$fields2[2*$_] => $values[$_]),  0 .. $lastfield);
}



sub MBR_2_partitions ($) {
  my $bootsect = shift;
  return # die "Expect to have \\x55\\xAA in MBR"
    unless length($bootsect) == 512 and "\x55\xAA" eq substr $bootsect, -2;
  # Up to offset 1BEh the MBR consists purely of machine code and data (strings
  # etc.). At offset 1BEh the first primary partition is defined, this takes 16
  # bytes, after which the second primary partition is defined, followed by
  # the third and fourth, the data structures are the same.

  my ($code, @parts) = unpack 'a446 a16 a16 a16 a16 v', $bootsect;
  my $check = pop @parts;

  # 00h 1 Set to 80h if this partition is active.
  # 01h 1 Partition's starting head.
  # 02h 2 Partition's starting [48]sector and track.
  # 04h 1 Partition's [49]ID number.
  # 05h 1 Partition's ending head.
  # 06h 2 Partition's ending [50]sector and track.
  # 08h 4 Starting LBA.
  # 0Ch 4 Partition's length in sectors.

  # Format of sector and track information.Bits 15-6 Bits 5-0
  #                                          Track    Sector
  # ID numbers:
  # 0Bh Win95 OSR2+ FAT32 (512MB-2TB)			(primary?)
  # 0Ch Win95 OSR2+ FAT32 (512MB-2TB LBA)			(extended?)
  my @part = (	# code	=> 'A446',
	      is_active		 => 'C',
	      start_head	 => 'C',
	      start_sec_track	 => 'v',
	      type		 => 'C',
	      end_head		 => 'C',
	      end_sec_track	 => 'v',
	      start_lba		 => 'V',
	      sectors		 => 'V',
	     );

  my @part_value = map { {raw => $_, decode_fields \@part, $_} } @parts;
  for my $p (@part_value) {
    $p->{start_sec}   = $p->{start_sec_track} & 0x3f;
    $p->{start_track} = $p->{start_sec_track} >> 6;
    $p->{end_sec}     = $p->{end_sec_track} & 0x3f;
    $p->{end_track}   = $p->{end_sec_track} >> 6;
  }
  ({bootcode => $code, signature => $check}, @part_value);
}

sub debug_partitions ($@) {
  my($fh, @partitions) = @_;
  my $n;
  for my $p (@partitions) {
    $n++;
    print $fh "  Part $n\n";
    print $fh "$_\t=> $p->{$_}\n" for sort keys %$p;
  }
}

# Experimental convertor from empty fat to empty fat32...
sub emit_fat32 ($$$$) {		# Also, essentially, seeks to bootsector
  my($p, $b, $emit_prefat32, $reader) = (shift, shift, shift, shift);

  my $offset_in_sectors = $p->{start_lba};
  die "The partition type is not defined" unless $p->{type};
  die "start_lba value is 0" unless $offset_in_sectors;

  substr($b, 446 + 4, 1) = chr 0x0B; # Win95 OSR2+ FAT32 (512MB-2TB) (primary?)
  die "Need emit_prefat32 defined too in presence of partition table"
    unless defined $emit_prefat32;
  open F32, "> $emit_prefat32" or die "Error opening `$emit_prefat32' for write: $!";
  binmode F32;
  syswrite F32, $b;
  if ($offset_in_sectors > 1) {
    my $in = $reader->(512*($offset_in_sectors - 1));
    syswrite F32, $in;
  }
  close F32 or die "Error closing `$emit_prefat32' for write: $!";
}

# Directory Entry Layout.
#
#    The old style directory entry had 10 reserved bytes starting at 0Ch,
#                            these are now used.
# 00h 8 Filename padded with spaces if required (see above).
# 08h 3 Filename extension padded with spaces if required.
# 0Bh 1 File Attribute Byte.
# 0Ch 10 Reserved or extra data.
# 16h 2 Time of last write to file (last modified or when created).
# 18h 2 Date of last write to file (last modified or when created).
# 1Ah 2 Starting cluster.
# 1Ch 4 File size (set to zero if a directory).
#
#
# Extra data Layout (previously reserved area).
#
#    The old style directory entry had 10 reserved bytes starting at 0Ch,
#     these are now used as follows. Presumably these fields are used if
#                                 non-zero.
#                            Offset Length Field
# 0Ch 1 Reserved for use by Windows NT.
# 0Dh 1 Tenths of a second at time of file creation, 0-199 is valid.
# 0Eh 2 Time when file was created.
# 10h 2 Date when file was created.
# 12h 2 Date when file was last accessed.
# 14h 2 High word of cluster number (always 0 for FAT12 and FAT16).

my @file_f = (	basename	=> 'A8',
		ext	=> 'A3',
		attrib	=> 'C',
		name_ext_case	=> 'C',
		creation_01sec	=> 'C',
		time_creation	=> 'v',
		date_create	=> 'v',
		date_access	=> 'v',
		cluster_high	=> 'v',
		time_write	=> 'v',
		date_write	=> 'v',
		cluster_low	=> 'v',
		size	=> 'V',
  );

my @lfn_f = (	seq_number	=> 'C',
		name_chars_1	=> 'a10',
		attrib	=> 'C',
		nt_reserved	=> 'C',
		checksum_dosname => 'C',
		name_chars_2	=> 'a12',
		cluster_low	=> 'v',
		name_chars_3	=> 'a4',
  );

my $nn = 0;
my %file_attrib = map +($_ => 1<<($nn++)),
  qw(is_readonly is_hidden is_system is_volume_label
     is_subdir is_archive is_device);

sub dos_chksum ($$) {
  my ($n,$ext,$sum) = (shift, shift, 0);
  $sum = ((($sum & 1)<<7) + ($sum >> 1) + ord $_) & 0xFF
    for split //, sprintf "%-8s%-3s", $n, $ext;
  $sum
}

sub interpret_directory ($$;$$$) {
  my ($dir, $is_fat32, $keep_del, $keep_dots, $keep_labels) =
    (shift, shift, shift, shift, shift);
  my ($res, @files, @lfn, $lfn_checksum, $lfn_seq, $lfn_tot, $lfn_del);
  while (length $dir) {
    $dir =~ s/^((.).{31})//s or die "short directory!";
    $res = 'end', last if $2 eq "\0";		# No entries after this point
    next if not $keep_del and 0xE5 == ord $2;	# deleted or not filled
    my %f = decode_fields \@file_f, $1;
    $f{deleted} = 0;
    $f{deleted} = 1 if 0xE5 == ord $2;		# deleted or not filled
    if ($f{attrib} == 0x0F) {	# LFN
      # next;
      %f = decode_fields \@lfn_f, $1;
      if (not $keep_del) {	# XXX How to process? Ignore seq numbers???
	@lfn = (), next if $f{seq_number} & 0x80; # Deleted entry
      } else {			# Deleted entry for non-deleted file?
	@lfn = (), next if $f{seq_number} & 0x80 and $f{seq_number} != 0xE5;
	if ($f{seq_number} == 0xE5) {
	  die "Deleted LFN subrecord in middle of LFN" if @lfn and !$lfn_del;
	  $lfn_del = 1;
	  $f{deleted} = 1;
	} else {		# Ignore deleted LFN preceeding a valid LFN
	  @lfn = () if @lfn and $lfn_del;
	  $lfn_del = 0;
	}
      }
      $f{raw} = $1;
      unless ($f{deleted}) {
	die "LFN start unexpected" if @lfn and $f{seq_number} & 0x40;
	die "LFN continuation unexpected" unless @lfn or $f{seq_number} & 0x40;

	die "LFN continuation out-of-order: $f{seq_number} after $lfn_seq"
	  if @lfn and $f{seq_number} != ($lfn_seq & ~0x40) - 1;
	$lfn_seq = $f{seq_number};

	die "Mismatch in checksums"
	  if @lfn and $lfn_checksum != $f{checksum_dosname};
      }
      $lfn_checksum = $f{checksum_dosname};
      $f{lfn_chars} = "$f{name_chars_1}$f{name_chars_2}$f{name_chars_3}";
      $f{lfn_chars} =~ s/\0\0(\xFF\xFF){0,11}$// # may be non-terminated...
	# or die "LFN `$f{lfn_chars}' not terminated by 0x0000"
	  unless @lfn;
      if (@lfn) {
	$lfn_tot = "$f{lfn_chars}$lfn_tot"
      } else {
	$lfn_tot = $f{lfn_chars}
      }
      push @lfn, \%f;
      next;
    }
    $f{raw} = $1;
    $f{basename} =~ s/^\x05/\xE5/;
    @lfn = (), next
      if not $keep_dots and $f{basename} =~ /^\.\.?$/ and $f{ext} eq ''; # . ..
    # DOSname is mangled for deleted files, so there is no point in checksum...
    @lfn = (), warn("Mis-attached LFN (chksum mismatch: $lfn_checksum vs `$f{basename}.$f{ext}')")
      if @lfn and not $lfn_del and $lfn_checksum != dos_chksum($f{basename}, $f{ext});
    next if ($f{attrib} & 0x08) and not $keep_labels;
    if ($is_fat32) {
      $f{cluster} = $f{cluster_low} + ($f{cluster_high} << 16);
    } else {
      $f{cluster} = $f{cluster_low};	# cluster_high has EA info?
    }
    $f{basename}  = lc $f{basename}  if $f{name_ext_case} & (1<<3);
    $f{ext}	  = lc $f{ext}	     if $f{name_ext_case} & (1<<4);
    my $ext = length $f{ext} ? ".$f{ext}" : '';
    $f{dos_name} = $f{name} = "$f{basename}$ext";
    $f{time_create} = $f{time_creation} + $f{creation_01sec}/100;
    $f{$_} = $f{attrib} & $file_attrib{$_} for keys %file_attrib;
    if (@lfn) {
      $f{lfn_raw} = [@lfn];
      $f{name} = join '', map chr, unpack 'v*', $lfn_tot;
      $f{lfn_name_UTF16} = $lfn_tot;
    }
    push @files, \%f;
    @lfn = ();
  }
  $res ||= 'mid' if @lfn;
  ($res, \@files);
}

# FAT12/FAT16 Boot Sector/Boot Record Layout.

#      The data contained in the boot sector after the OEM name string is
#                referred to as the BIOS parameter block or BPB.
# Offset Length                              Field
#  00h        3               Machine code for jump over the data.
#  03h        8        OEM name string (of OS which formatted the disk).
#  0Bh        2   Bytes per sector, nearly always 512 but can be 1024,2048 or
#                                            4096.
#  0Dh        1 Sectors per cluster, valid number are: 1,2,4,8,16,32,64 and 128,
#                     but a cluster size larger than 32K should not occur.
#  0Eh        2     Reserved sectors (number of sectors before the first FAT
#                            including the boot sector), usually 1.
#  10h        1                Number of FAT's (nearly always 2).
#  11h        2            Maximum number of root directory entries.
#  13h        2  Total number of sectors (for small disks only, if the disk is
#                  too big this is set to 0 and offset 20h is used instead).
#  15h        1  Media descriptor byte, pretty meaningless now (see below).
#  16h        2                         Sectors per FAT.
#  18h        2                        Sectors per track.
#  1Ah        2                   Total number of heads/sides.
#  1Ch        4   Number of hidden sectors (those preceding the boot sector).
#  20h        4             Total number of sectors for large disks.
	# Starts FAT12/16-specific
#  24h       26       Either extended BPB (see below) or machine code.
#  3Eh      448                          Machine code.
#  1FEh       2                      Boot Signature AA55h.

	# Starts FAT32-specific
# 0x24              4 Sectors per file allocation table
# 0x28              2 FAT Flags
# 0x2a              2 Version
# 0x2c              4 Cluster number of root directory start
# 0x30              2 Sector number of FS Information Sector
# 0x32              2 Sector number of a copy of this boot sector
# 0x34             12 Reserved
# 0x40              1 Physical Drive Number
# 0x41              1 Reserved
# 0x42              1 Extended boot signature.
# 0x43              4 ID (serial number)
# 0x47             11 Volume Label
# 0x52              8 FAT file system type: "FAT32   "
# 0x5a            420 Operating system boot code
#  0x1FE              2 Boot sector signature (0x55 0xAA)


my @boot_c = (	jump	=> 'A3',
		oem	=> 'A8',
		sector_size	=> 'v',
		sectors_in_cluster	=> 'C',
		FAT_table_off	=> 'v',
		num_FAT_tables	=> 'C',
		root_dir_entries	=> 'v',
		total_sectors1	=> 'v',
		media_type	=> 'C',
		sectors_per_FAT16	=> 'v',
		sectors_per_track	=> 'v',
		heads	=> 'v',
		hidden_sectors	=> 'V',
		total_sectors2	=> 'V',
    );
my @boot_16 = (	extended_bpb	=> 'a26',
		machine_code	=> 'a448',
		boot_signature	=> 'v',
    );
my @boot_32 = (	sectors_per_FAT32	=> 'V',
		FAT_flags	=> 'v',
		version	=> 'v',
		rootdir_start_cluster	=> 'V',
		fsi_sector_sector	=> 'v',
		bootcopy_sector_sector	=> 'v',
		reserved1	=> 'a12',
		physical_drive	=> 'C',
		reserved2	=> 'C',
		ext_boot_signature	=> 'C',
		serial_number	=> 'V',
		volume_label	=> 'A11',
		FS_type	=> 'A8',
		machine_code	=> 'a420',
		boot_signature	=> 'v',
    );


# FAT12/Fat16 Extended BPB.
#
#     The Extended BIOS parameter block is not present prior to DOS 4.0
#                              formatted disks.
#                                   Offset
#                             Length (in bytes)
#                                   Field
# 24h 1 Physical drive number (BIOS system ie 80h is first HDD, 00h is first FDD).
# 25h 1 Current head (not used for this; WinNT bit 0 is a dirty flag to request chkdsk at boot time. bit 1 requests surface scan too).
# 26h 1 Signature (must be 28h or 29h to be recognised by NT).
# 27h 4 The serial number, the serial number is stored in reverse order
#          and is the hex representation of the bytes stored here.
# 2Bh 11 Volume label.
# 36h 8 File system ID. "FAT12", "FAT16" or "FAT  ".

# Further structure used by FAT32:
# Byte Offset Length (bytes)                 Description
#        0x24              4 Sectors per file allocation table
#        0x28              2 FAT Flags
#        0x2a              2 Version
#        0x2c              4 Cluster number of root directory start
#        0x30              2 Sector number of FS Information Sector
#        0x32              2 Sector number of a copy of this boot sector
#        0x34             12 Reserved
#        0x40              1 Physical Drive Number
#        0x41              1 Reserved
#        0x42              1 Extended boot signature (0x28 0x29).
#        0x43              4 ID (serial number)
#        0x47             11 Volume Label
#        0x52              8 FAT file system type: "FAT32   "
#        0x5a            420 Operating system boot code
#       0x1FE              2 Boot sector signature (0x55 0xAA)

my @e_boot = (
		physical_drive	=> 'C',
		head___dirty_flags	=> 'C',
		ext_boot_signature	=> 'C',
		serial_number	=> 'V',
		volume_label	=> 'A11',
		FS_type		=> 'A8',
    );

# FS Information Sector
# Byte Offset Length (bytes)                          Description
#        0x00              4 FS information sector signature (0x52 0x52 0x61 0x41 / "RRaA")
#        0x04            480 Reserved (byte values are 0x00)
#       0x1e4              4 FS information sector signature (0x72 0x72 0x41 0x61 / "rrAa")
#       0x1e8              4 Number of free clusters on the drive, or -1 if unknown
#       0x1ec              4 Number of the most recently allocated cluster
#       0x1f0             14 Reserved (byte values are 0x00)
#       0x1fe              2 FS information sector signature (0x55 0xAA)

sub preprocess_bootsect ($) {
  my $s = shift;
  $s->{total_sectors} = $s->{total_sectors1} || $s->{total_sectors2};
  $s->{sectors_per_FAT} = $s->{sectors_per_FAT32} || $s->{sectors_per_FAT16};
  $s->{pre_sectors} = $s->{FAT_table_off}
    + $s->{num_FAT_tables} * $s->{sectors_per_FAT}
      + $s->{root_dir_entries} * 0x20 / $s->{sector_size};
  $s->{sector_of_cluster0} = $s->{pre_sectors} - 2*$s->{sectors_in_cluster};
  $s->{last_cluster} = int(($s->{total_sectors} - $s->{pre_sectors}
			   + $s->{sectors_in_cluster} - 1)/$s->{sectors_in_cluster}) + 2;
}

sub guess_width ($$) {
  my ($s, $raw) = (shift, shift);
  my $w = 12;
  my %bpb = decode_fields \@e_boot, $s->{extended_bpb};
  if ($s->{last_cluster} >= 0x10000 or $s->{root_dir_entries} == 0
      or $bpb{ext_boot_signature} != 0x28 and $bpb{ext_boot_signature} != 0x29) {
    $w = 32;
    %$s = decode_fields [@boot_c, @boot_32], $raw;
    preprocess_bootsect $s;
  } elsif ($s->{last_cluster} >= 0x1000) {
    $w = 16
  } else {			# Any other way to determine width???
  }
  $s->{bpb_ext_boot_signature} = $bpb{ext_boot_signature};
  @$s{keys %bpb} = values %bpb unless $w == 32;
  $s->{guessed_FAT_flavor} = $w;
  $s->{raw} = $raw;
}

sub interpret_bootsector ($) {
  my $bootsect = shift;
  my $s = {decode_fields [@boot_c, @boot_16], $bootsect};
  preprocess_bootsect $s;
  guess_width $s, $bootsect;
  $s
}

sub check_bootsector ($;$) {
  my $s = shift;
  # Expected size of FAT with 12bit per entry
  my $exp = $s->{last_cluster} * $s->{guessed_FAT_flavor}/8/$s->{sector_size};
  die "FAT has $s->{sectors_per_FAT} sectors: expecting $exp"
    unless $s->{sectors_per_FAT} >= $exp;
  warn "FAT has $s->{sectors_per_FAT} sectors: expecting $exp"
    unless $s->{sectors_per_FAT} <= $exp + 10;

# How to distinguish bootsector from MBR?  Jump on FAT12 is "EB 3C 90";
# 0x90 is NOP, 0xEB is jump(displacement8).  In FAT32, there are extra
# 28 bytes, so displacement should be 0x58.  To be extra safe (e.g., allow
# chymera bootsector-and-MBR), one should tolerate other jumps...

  die sprintf "Unexpected bootsector: first byte %#02x\n",
    ord substr $s->{raw},0,1 
      if shift and not $s->{raw} =~ /^\xEB/; # Check JMP instruction
  return 1 if
    ($s->{ext_boot_signature} == 0x28 or $s->{ext_boot_signature} == 0x29)
      and $s->{boot_signature} == 0xAA55
	and $s->{FS_type} =~ /^fat(\d{2})?/i
	  and (not $1 or $1 eq $s->{guessed_FAT_flavor});
  die <<EOD
Unexpected bootsector: guessed_width=$s->{guessed_FAT_flavor}, last_cluster=$s->{last_cluster}, root_dir_entries=$s->{root_dir_entries},
  boot_signature=$s->{boot_signature}, ext_boot_signature16=$s->{bpb_ext_boot_signature}, ext_boot_signature=$s->{ext_boot_signature}, FS_type=$s->{FS_type}
EOD
}

sub string_to_n ($$$$) {
  my($s, $offset, $n, $w) = (shift, shift, shift, shift);
  my($n2, $w2) = ($n, ($w>>3));
  $n2 >>= 1, $w2 = 3 if $w == 12;
  $offset += $w2 * $n2;
  my $out = unpack 'V', substr($$s, $offset, $w2) . "\0\0";
  if ($w == 12) {
    if ($n % 2) {
      $out >>= 12
    } else {
      $out &= 0xFFF
    }
  }
  $out
}

sub FAT_2array ($$$;$$) {
  my($fat, $s, $w, $offset, $lim) = (shift, shift, shift, shift || 0, shift);
  $lim = length($$s) - $offset unless defined $lim;
  die "Too large offset=$offset, lim=$lim" if $lim + $offset > length $$s;
  if ($w == 12) {
    $lim += $offset;
    while ($offset < $lim) {
      my $ss = substr $$s, $offset, 3;
      my $n32 = unpack 'V', "$ss\0";
      # warn sprintf "got %#04x\n", $n32;
      push @$fat, ($n32 & 0xFFF), ($n32 >> 12);
      $offset += 3;
    }
  } else {
    my $f = ($w == 32) ? 'V' : 'v';
    $w >>= 3;
    $lim = int($lim/$w);
    # Do not extend stack too much:
    while ($lim >= 1) {
      my $l = ($lim > 1000) ? 1000 : $lim;
      push @$fat, unpack "x$offset $f$l", $$s;
      $lim -= $l;
      $offset += $l * $w;
    }
  }
  # warn "FAT = @$fat\n"
}

sub check_FAT_array ($$;$) {
  my ($fat, $b, $offset, @fat) = (shift, shift, shift || 0);
  FAT_2array(\@fat, $fat, $b->{guessed_FAT_flavor}, $offset, 2*4),
    $fat = \@fat unless 'ARRAY' eq ref $fat; # Make into array

  my $max_cluster = (1<<$b->{guessed_FAT_flavor}) - 1;
  die sprintf "Wrong signature %d=%#x, media=%#x in cluster(0)",
    $fat->[0], $fat->[0], $b->{media_type}
      unless $fat->[0] == (($b->{media_type} | 0xffffff00) & $max_cluster);

  my $eof = $fat->[1];		# Leading 0 in FAT32:
  die sprintf "Wrong signature %d=%#x in cluster(1)", $eof, $eof
    unless ($eof >> 3) == ($max_cluster >> (3 + 4*(32==$b->{guessed_FAT_flavor})));
  return 1;
}

sub cluster_chain ($$$$;$$) {
  my ($cluster, $maxc, $fat, $b, $compress, $offset) = (shift, shift, shift, shift, shift, shift||0);
  my $last_cluster = $b->{last_cluster};
  die "problem with cluster=$cluster as a cluster leader"
    unless $cluster >= 2 and $cluster <= $last_cluster;
  my ($c, @clusters) = (1, $cluster);
  my $w = $b->{guessed_FAT_flavor};
  my $stop_3 = (1<<($w - 3 - 4*($w==32))) - 1; # Leading 0 in FAT32
  my $total = 1;
  my $subr = ($compress and ref $compress eq 'CODE' and $compress);
  while (--$maxc) {
    # warn "processing $cluster, rem=$maxc, stop_3=$stop_3, w=$w";
    my $next;
    if (ref $fat eq 'ARRAY') {
      $next = $fat->[$cluster];
    } else {			# A reference to 'V*'-string
      $next = string_to_n($fat, $offset, $cluster, $w);
    }
    if ($compress) {
      $c++, next if $next == ++$cluster;
      if ($subr) {
	$subr->($clusters[-1], $c);
	pop @clusters;
      } else {
	push @clusters, $c;
      }				# New cluster would be inserted later
      $total += $c - 1;
      $c = 1;
    }
    return $total, \@clusters if ($next >> 3) == $stop_3;
    $next = 'undef' unless defined $next; # XXX ???
    die "problem with cluster(+1)=$cluster => $next in a cluster chain"
      unless $next >= 2 and $next <= $last_cluster;
    $total++, push @clusters, $cluster = $next;
  }
  return 0, \@clusters
}

sub min($$){my($a,$b)=@_;$a>$b? $b:$a}

sub seek_and_read ($$$$;$) {
  my ($fh, $seek, $read) = (shift,shift,shift);
  sysseek $fh, $seek, 0 or die "sysseek $seek: $!" if defined $seek;
  $_[0]=' ', $_[0] x= $read, $_[0] = '' unless defined $_[0];
  die "seek_and_read outside of string" if ($_[1] || 0) > length $_[0];
  substr($_[0], $_[1] || 0) = '';
  my($r,$t,$c) = ($read, 0);
  $r -= $c, $t += $c
    while $r and $c = sysread $fh, $_[0], min($r, $lim_read), length $_[0];
  die "Short read ($t instead of $read)" unless $t == $read;
  1;
}

sub read_FAT_data ($$;$$$) {
  my ($fh, $how, $offset, $b, $FAT) = (shift, shift, shift||0, shift, shift);
  my ($close, $inif, $out, $mbr, $b_read);
  unless (ref $fh) {
    open IN, '<', $inif = $fh or die "open `$fh' for read: $!";
    $fh = \*IN;
    $close = 1;
  }
  binmode $fh;
  if (defined $how->{do_MBR}) {
    seek_and_read $fh, $offset, 512, $mbr;
    if ($how->{do_MBR} eq 'maybe' and defined $how->{do_bootsector}) {
      eval { my $b1 = interpret_bootsector $mbr;
	     check_bootsector $b1;
	     $out->{bootsect_off} = $offset;
	     $b = $out->{bootsector} = $b1;
	     $b_read = 1 } and undef $mbr;
    }
    if ($mbr and (defined $how->{parse_MBR} or defined $how->{do_bootsector}
		  or defined $how->{do_rootdir} or defined $how->{do_FAT})) {
      my($fields, @p) = MBR_2_partitions $mbr or die "Wrong signature in MBR";
      my @valid = defined $how->{partition} ? $how->{partition} : (0..3);
      # Type = 0 is Empty; FreeSpace is not marked as a partition???
      @valid = grep $p[$_]{start_lba} && $p[$_]{sectors} && $p[$_]{type}, @valid;
      unless (@valid) {
	die "Partition $how->{partition} invalid" if $how->{partition};
	die "No valid partition found";
      }
      die "Too many valid partitions: @valid" if @valid > 1;
      $offset += $p[$valid[0]]{start_lba} * 512;
      $out->{mbr} = {%$fields, partitions => \@p};
    }
  }
  if (defined $how->{do_bootsector} and not $b_read) {
    die "Bootsector given as argument and needs to be read too?" if $b;
    seek_and_read $fh, $offset, 512, my $bs;
    if (defined $how->{parse_bootsector} or defined $how->{do_rootdir}
	or defined $how->{do_FAT}) {
      $b = interpret_bootsector $bs;
      check_bootsector $b;
    } else {
      $b = {raw => $bs};
    }
    $out->{bootsector_offset} = $offset;
    $out->{bootsector} = $b;
  }
  if (defined $how->{do_FAT}) {
    die "need bootsector" unless $b;
    die "FAT given as argument and needs to be read too?" if $FAT;
    my $o = $offset;
    $o += ($b->{FAT_table_off} + $how->{do_FAT} * $b->{sectors_per_FAT})
      * $b->{sector_size} unless $how->{FAT_separate};
    die "FAT[$how->{do_FAT}] not present: only $b->{num_FAT_tables} FAT table"
      if $b->{num_FAT_tables} <= $how->{do_FAT};
    seek_and_read $fh, $o, $b->{sector_size} * $b->{sectors_per_FAT}, my $F;
    if (defined $how->{parse_FAT}
	and $b->{last_cluster} < ($how->{parse_FAT} || 3e6)) {
      my @f;
      $#f = $b->{last_cluster};
      @f = ();
      FAT_2array(\@f, \$F, $b->{guessed_FAT_flavor});
      $FAT = \@f;
    } else {
      $FAT = \$F;
    }
    $out->{FAT} = $FAT;
    $out->{FAT_raw} = \$F if $how->{raw_FAT} or not defined $how->{parse_FAT};
  }
  if (defined $how->{do_rootdir}) {
    die "need bootsector" unless $b;
    my($s, $l, $o) = '';
    if ($how->{rootdir_is_standalone}) {
      local $/;
      $s = <$fh>;
    } else {
      my($L, $S) = ($b->{sector_size} * $b->{sectors_in_cluster},
		      $offset + $b->{sector_of_cluster0}*$b->{sector_size});
      if ($b->{guessed_FAT_flavor} == 32) {
	my $appender = sub ($$) {
	  my($start, $len) = (shift, shift);
	  seek_and_read $fh, $S + $L * $start, $len * $L, $s, length $s;
	}
	;
	if ($FAT) {
	  cluster_chain($b->{rootdir_start_cluster}, 0, $FAT, $b, $appender);
	} else {
	  $appender->($b->{rootdir_start_cluster}, 1); # XXX Assume 1 cluster
	}
      } else {
	my $off = ($offset + $b->{sector_size} *
		   ($b->{FAT_table_off} + $b->{num_FAT_tables} * $b->{sectors_per_FAT}));
	seek_and_read $fh, $off, $b->{root_dir_entries} * 0x20, $s;
      }
    }
    if (defined $how->{parse_rootdir}) {
      my($res, $f) = interpret_directory $s, $b->{guessed_FAT_flavor} == 32,
	$how->{keep_del}, $how->{keep_dots}, $how->{keep_labels};
      die "Directory ended in the middle of LFN" if ($res || 0) eq 'mid';
      $out->{rootdir_files} = $f;
      $out->{rootdir_ended} = $res;
    }
    $out->{rootdir_raw} = $s;
  }
  close $fh or die "close `$inif' for read: $!" if $close;
  return $out;
}

sub output_cluster_chain ($$$$$$;$) {
  my($ifh, $ofh, $start, $size, $b, $FAT, $offset) = 
    (shift, shift, shift, shift, shift, shift, shift||0);
  return unless $size;
  my($L, $S) = ($b->{sector_size} * $b->{sectors_in_cluster},
		$offset + $b->{sector_of_cluster0}*$b->{sector_size});
  my $piper = sub ($$) {
    my($start1, $len) = ($L * shift, $L * shift);
    # warn "Piper: start=$start1, len=$len\n";
    if ($len > $size) {
      die "Cluster chain too long, len=$len, cl=$L, sz=$size" if $len - $L >= $size;
      $len = $size;
    }
    while ($len) {
      my $l = ($len > (1<<24)) ? (1<<24) : $len; # 16M chunks
      my $s;
      seek_and_read $ifh, $S + $start1, $l, $s;
      syswrite $ofh, $s, length $s;
      $len -= $l, $size -= $l, $start1 += $l;
    }
  };
  my $sz = int(($size + $L - 1)/$L);
  $piper->($start, $sz), return 1 if not defined $FAT;
  # Inspect the last cluster for end of chain too
  my ($total) = cluster_chain $start, $sz+1, $FAT, $b, $piper;
  die "No end of cluster chain" unless $total;
  1;
}

sub read_cluster_chain ($$$$;$$) { # No size, as in dir...
  my($ifh, $start, $b, $FAT, $offset, $exp_len) =
    (shift, shift, shift, shift, shift||0, shift);
  my($L, $S, $s) = ($b->{sector_size} * $b->{sectors_in_cluster},
		    $offset + $b->{sector_of_cluster0}*$b->{sector_size}, '');
  (seek_and_read $ifh, $S + $L * $start, $exp_len, $s),
    return \$s if not defined $FAT and defined $exp_len;
  my $piper = sub ($$) {
    my($start1, $l) = (shift, shift);
    seek_and_read $ifh, $S + $L * $start1, $L * $l, $s, length $s;
  };
  my ($total) = cluster_chain $start, 0, $FAT, $b, $piper;
  die "No end of cluster chain" unless $total;
  \$s;
}

sub write_file ($$$$$;$) {
  my ($fh, $dir, $f, $b, $FAT, $offset) =
    (shift, shift, shift, shift, shift, shift||0);
  return if $f->{is_volume_label}
    or $f->{name} eq 'EA DATA. SF' or $f->{name} eq 'WP ROOT. SF';
  die "directory `$f->{name}' as file!" if $f->{is_subdir};
  my $name = "$dir/$f->{name}";
  open O, '>', $name or die "error opening $name for write: $!";
  binmode O;
  output_cluster_chain($fh, \*O, $f->{cluster}, $f->{size}, $b, $FAT, $offset);
  close O or die "error closing $name for write: $!";
  chmod 0555, $name if $f->{attrib} & 0x1;	# read only
  # unset archive mode?
}

sub recurse_dir ($$$$$$$;$);
sub recurse_dir ($$$$$$$;$) {
  my ($callbk, $path, $fh, $how, $f, $b, $FAT, $offset) =
    (shift, shift, shift, shift, shift, shift, shift, shift||0);
  my $files =
    interpret_directory( $$f, $b->{guessed_FAT_flavor} == 32, $how->{keep_del},
			 $how->{keep_dots}, $how->{keep_labels} );
  for my $file (@$files) {
    # next if $file->{is_volume_label};
    my $res = $callbk->($path, $file);
    if ($res and $file->{is_subdir} and not $file->{deleted}
	and $file->{name} !~ /^\.(\.)?$/) {
      push @$path, $file->{name};
      my $exp_len;
      $exp_len = $b->{sector_size} * $b->{sectors_in_cluster}
	unless defined $FAT;	# XXXX Expect dir size of one cluster???
      recurse_dir($callbk, $path, $fh, $how,
		  read_cluster_chain($fh, $file->{cluster}, $b, $FAT, $offset,
				     $exp_len),
		  $b, $FAT, $offset);
      pop @$path;
    }
  }
}

sub write_dir ($$$$$;$$$$) {
  my ($fh, $o_root, $ff, $b, $FAT, $how, $depth, $offset, $exists) =
    (shift, shift, shift, shift, shift, shift, shift||0, shift);
  $depth = 1e100 unless defined $depth;
  my $callbk = sub ($$) {
    my($path,$f) = (shift, shift);
    next if $f->{is_volume_label} or $f->{name} =~ /^\.(\.)?$/;
    my $p = join '/', $o_root, @$path;
    return write_file $fh, $p, $f, $b, $FAT, $offset unless $f->{is_subdir};
    return 0 if @$path >= $depth;
    mkdir "$p/$f->{name}", 0777 or die "mkdir `$p/$f->{name}': $!"
      unless $exists and not @$path;
    return 1;
  };
  recurse_dir($callbk, [], $fh, $how||{}, $ff, $b, $FAT, $offset);
}

sub list_dir ($$$$;$$$) {
  my ($fh, $ff, $b, $FAT, $how, $depth, $offset) =
    (shift, shift, shift, shift, shift, shift, shift||0, shift);
  $depth = 1e100 unless defined $depth;
  my $callbk = sub ($$) {
    my($path,$f,$pre) = (shift, shift, '');
    print("# label=$f->{name}\n"), return if $f->{is_volume_label};
    my $p = join '/', @$path, $f->{name};
    $p .= '/' if $f->{is_subdir};
    $pre = '#del ' if $f->{deleted};
    $pre = '# ' if $f->{name} =~ /^\.(\.)?$/;
    print "$pre$f->{attrib}\t$f->{size}\t$f->{date_write}/$f->{time_write}\t$f->{cluster}\t$p\n";
    return @$path < $depth;
  };
  recurse_dir($callbk, [], $fh, $how||{}, $ff, $b, $FAT, $offset);
}

# First FAT entry contains 0xFF*, the rest 0x0F*; so 0x2*, 0xA* do not conflict
sub compress_FAT ($$$) {	# Down to 2-4 bytes/file after gzip...
  my($FAT, $w, $fh) = (shift, shift, shift);
  my ($c, $cc, $c0, $off, $ee, $remain, @out, $F) = (0, 0, 0, 0);
  local $\ = '';
  while (1) {
    if (ref $FAT eq 'ARRAY') {
      $F = $FAT, $remain = 0;
    } else {
      $remain = length $$FAT unless defined $remain;
      my $l = $remain;
      $l = 750000 if $l > 750000; # Should be divisible by 12...
      FAT_2array($F = [], $FAT, $w, $off, $l);
      $remain -= $l, $off += $l;
    }
    for my $e (@$F) {
      $c++;			# Next cluster
      if ($e) {
	(push @out, 0xA0000000 + $c0), $c0 = 0 if $c0;
	$cc++, next if $e == $c;
	(push @out, 0x20000000 + $cc), $cc = 0 if $cc;
	push @out, $e;
      } else {
	(push @out, 0x20000000 + $cc), $cc = 0 if $cc;
	$c0++, next;
      }
      (print $fh pack 'V*', @out), @out = () if @out > 1000;
    }
    last unless $remain;
  }
  push @out, 0xA0000000 + $c0 if $c0;
  push @out, 0x20000000 + $cc if $cc;
  print $fh pack 'V*', @out;
}

sub _FAT_2string ($$$$) {
  my($FAT, $w, $start, $c) = @_;
  if ($w eq 12) {
    my($out, $e) = ('', $start + $c);
    while ($start < $e) {	# Assume even
      my $x = pack 'V', $FAT->[$start] + ($FAT->[$start+1]<<12);
      $out .= substr $x, 0, 3;
      $start += 2;
    }
    $out;
  } else {	# $w is 'V' or 'v'
    pack "$w*", @$FAT[$start .. $start + $c - 1];
  }
}

sub output_FAT ($$$) {
  my($FAT, $w, $fh, $s) = (shift, shift, shift, 0);
  local $\ = '';
  (print $fh $$FAT), return unless ref $FAT eq 'ARRAY';
  my $c = @$FAT;
  $w = (32 == $w) ? 'V' : 'v' if $w != 12;
  while ($c) {
    my $cc = ($c > 750000) ? 750000 : $c;
    print $fh _FAT_2string($FAT, $w, $s, $cc);
    $c -= $cc, $s += $cc;
  }
}

sub __emit ($$$) {
  my($ofh, $out, $w) = (shift, shift, shift);
  my $outc = @$out;
  my $cut;
  $cut = 1, $outc-- if $w eq 12 and $outc % 2;
  print $ofh _FAT_2string($out, $w, 0, $outc);
  @$out = $cut ? $$out[-1] : ();
}

sub uncompress_FAT ($$$) {
  my($ifh, $w, $ofh) = (shift, shift, shift);
  my ($c, @f, @out, $F) = (0);
  @f[0x2, 0xA] = (0x2, 0xA);
  local $\ = '';
  $w = (32 == $w) ? 'V' : 'v' if $w != 12;
  while (1) {
    last unless sysread $ifh, $F, 4*1e4;
    for my $n (unpack 'V*', $F) {
      my $n1 = $f[$n >> 28];
      if ($n1) {		# Special
	my $cc = $n & 0xFFFFFFF;
	while ($cc) {
	  my ($ccc, @rest) = $cc;
	  $ccc = 1e4 if $ccc > 1e4;
	  if ($n1 == 0x2) {	# A run
	    push @out, $c + 1 .. $c + $ccc;
	  } else {		# 0s
	    push @out, (0) x $ccc;
	  }
	  $cc -= $ccc, $c += $ccc;
	  __emit($ofh, \@out, $w) if @out >= 1e4;
	}
      } else {
	$c++;
	push @out, $n;
      }
    }
    __emit($ofh, \@out, $w) if @out >= 1e4;
  }
  __emit($ofh, \@out, $w);
  die "Odd number of uncompressed items, w=$w" if @out;
}

1;
__END__

=head1 NAME

FileSystem::LL::FAT - Perl extension for low-level access to FAT partitions

=head1 SYNOPSIS

  use FileSystem::LL::FAT;
  blah blah blah

=head1 DESCRIPTION

=head2 MBR_2_partitions($sector)

  ($fields, @partitions) = MBR_2_partitions($sector) or die "Not an MBR";

Takes the first sector as a string, extracts the partition info and
other information.  Currently the only fields in the hash referenced by
$fields is C<bootcode> (string of length 446) and C<signature> (0xAA55).

Each element of @partitions is a hash reference with fields

  raw is_active start_head start_sec_trac type end_head end_sec_track
  start_lba sectors start_sec end_sec start_trac end_track

Returns an empty list unless signature is correct.

=head2 interpret_bootsector($bootsector)

Takes a string containing 512Byte bootsector; returns a hash reference
with decoded fields.  The keys include

  jump oem sector_size sectors_in_cluster FAT_table_off num_FAT_tables
  root_dir_entries total_sectors1 media_type sectors_per_FAT16
  sectors_per_track heads hidden_sectors total_sectors2
  machine_code FS_type boot_signature volume_label physical_drive
  ext_boot_signature serial_number raw

  bpb_ext_boot_signature guessed_FAT_flavor
  total_sectors sectors_per_FAT pre_sectors last_cluster sector_of_cluster0

(the last line contains info calculated based on other entries;
C<guessed_FAT_flavor> is one of 12,16,32, and
C<bpb_ext_boot_signature> is the C<ext_boot_signature> calculated
assuming FAT12 or FAT16 layout of bootsector).

Additional flavor-dependent keys: in FAT32 case

  sectors_per_FAT32 FAT_flags version rootdir_start_cluster
  fsi_sector_sector bootcopy_sector_sector reserved1 reserved2

otherwise

  extended_bpb head___dirty_flags

=head2 check_bootsector($fields)

Takes a hash reference with decoded fields of a bootsector; returns
TRUE if minimal sanity checks hold; die()s otherwise.

=head2 interpret_directory($dir, $is_fat32, [$keep_del, [$keep_dots, [$keep_labels]]])

  ($res, $files) = interpret_directory($dir, $is_FAT32);
  $files = interpret_directory($dir, $is_FAT32);

Takes catenation of directory cluster(s) as a string, extracts
information about the files in the directory.  Each element of array
referenced by $files is a hash reference with keys

  raw basename ext attrib name_ext_case creation_01sec time_create
  date_create date_access cluster_high time_write date_write cluster_low
  size cluster name dos_name time_creation
  is_readonly is_hidden is_system is_volume_label is_subdir is_archive is_device

and possibly C<lfn_name>, C<lfn_name_UTF16>, C<lfn_raw> (if
applicable).  (The last row lists flags extracted from C<attrib>.)

C<basename> and $<ext> are parts of the "DOS name" (lowercased if
indicated by the flags), C<time_create> has 0.01sec granularity (while
C<time_creation> has 2sec granularity).  Entries for deleted files are
filtered out unless $keep_del is TRUE; F<.> and F<..> are also
filtered out unless $keep_dots is TRUE; records representing volume
labels are also deleted unless $keep_labels is TRUE.  If not filtered
out, hashes for deleted files have an extra key C<deleted> with a true
value.

C<lfn_raw> contains an array reference with all the fractional entries
which contain the Long File Name.  Each of them is a hash reference
with keys

  raw seq_number name_chars_1 attrib nt_reserved checksum_dosname
  name_chars_2 cluster_low name_chars_3 name_chars

$res is C<'end'> if end-of-directory entry is encountered; it is
C<'mid'> if directory ends in middle of LFN info.  Otherwise $res is
not defined.

=head2 FAT_2array($fat, $s, $w [, $offset [, $lim ] ] )

Takes a reference $s to a string, at offset $offset of which is the
string representation of the FAT table; the length of FAT table in
bytes is assumed to be $lim.  $offset defaults to 0, $lim defaults to
go to the end of string.

Appends to the array referenced by $fat a numeric array representating
FAT.  $w is the bitwidth of the field (in 12,16,32).

=head2 check_FAT_array($fat, $b [, $offset ])

$fat is a reference to a numeric array, or to the string containing
the representation of FAT at $offset (which defaults to 0).  $b is a
hash reference with keys C<guessed_FAT_flavor>, C<media_type> (e.g.,
the result of interpret_bootsector()).

Returns TRUE if the first two clusters satisfy the FAT conventions;
otherwise die()s.

=head2 cluster_chain($cluster, $maxc, $fat, $b [, $compress [, $offset ] ])

 ($total, $chain) = cluster_chain($cluster, $maxc, $fat, $b, $offset);

$fat is a reference to numeric array, or to the string containing the
representation of FAT at $offset (which defaults to 0).  $cluster is
the start cluster, $maxc is the maximal number of clusters to look for
(0 meaning no limit).  $b is a hash reference with keys
C<guessed_FAT_flavor>, C<last_cluster> (e.g., the result of
interpret_bootsector()).

$chain is an array reference with the clusters in the chain.  $total
is FALSE if no end-of-a-chain marker was seen; otherwise it contains
the total number of clusters.

If $compress is TRUE (defaults to FALSE), the cluster chain is
run-compressed: each continuous run of clusters is converted to a pair
of numbers: the starting cluster number, and length in clusters.  If
$compress is a subroutine reference, then it is called with these
numbers as arguments; otherwise these numbers are pushed into $chain.

=head2 read_FAT_data($fh, $how [, $offset, $b, $FAT ])

  $hash = read_FAT_data($fh, $how [, $offset, $b, $FAT ]);

Extracts one or more of MBR, bootsector, FAT table, root directory
from a file $fh containg "contents of a disk".  $fh may be a reference
to a file handle, or a name of the file.  The optional argument
$offset is the offset inside the file of the first entry to extract,
or of bootsector (default 0).

The hash reference $how contains extraction instructions.  If values
of keys C<do_MBR>, C<do_bootsector>, C<do_FAT>, C<do_rootdir> are
defined, the corresponding parts of filesystem are read.  If
C<do_MBR>'s value is C<'maybe'> and C<do_bootsector>'s is defined, the
MBR part is checked whether it is an actual MBR or a bootsector.  The
actual value of the key C<do_FAT> chooses the copy of FAT to work
with.

The value of key C<partition> governs which partition of 0..3 to
choose (only primaries are currently supported); if not defined, and
the number of valid partitions differs from 1, the call die()s.

If the value of key C<FAT_separate> is TRUE, $offset is the offset of
the start of (the first) FAT in the file; otherwise it is the offset
of MBR or bootsector (offsets of other parts are calculated as
needed).  If the value of kye C<rootdir_is_standalone> is TRUE,
rootdir is assumed to be the whole content of the file.

If the values of keys C<parse_MBR>, C<parse_bootsector>, C<parse_FAT>,
C<parse_rootdir> are defined (or this is needed for processing of
remaining parts to extract), the corresponding read parts are
interpreted as in MBR_2_partitions(), interpret_bootsector(),
FAT_2array(), interpret_directory().

The corresponding parsed values are put into C<$hash-E<gt>{MBR}>,
C<$hash-E<gt>{bootsector}>; if not parsed, the values are hash
references C<{raw =E<gt> STRING}>.  C<$hash-E<gt>{FAT}> is suitable
for argument of cluster_chain(): it is either a reference to string
representation of the FAT, or to array representation of FAT.

(To avoid overflowing the memory) the FAT is converted to array only
if C<parse_FAT> is defined, I<AND> the number of clusters is below a
certain limit.  The limit is the value of C<parse_FAT> unless 0; if 0,
the default value 3000000 is used (the corresponding memory usage for
array FAT representation is about 60MB).

When bootsector is read, C<$hash-E<gt>{bootsector_offset}> is the actual
offset of bootsector (useful if $offset is actually referencing an
MBR).  Finally, if C<parse_rootdir>'s value is defined,
C<$hash-E<gt>{rootdir_files}> is a reference to array of files in the
root directory, C<$hash-E<gt>{rootdir_ended}> is true if
end-of-directory marker was seen (i.e., the directory ends before the
end of the allocated space); anyway, C<$hash-E<gt>{rootdir_raw}> is
string representation of the root directory.

The keys C<keep_del>, C<keep_dots>, C<keep_labels> are given as
corresponding arguments to interpret_directory().  If values
referenced by C<raw_FAT> is TRUE, or by C<parse_FAT> is undefined,
C<$hash-E<gt>{FAT_raw}> contains a reference to the string
representation of FAT.

=head2 write_dir($fh, $o_root, $d, $b, $FAT, [$how, $depth, $offset, $exists])

recursively extract the content of directory $d (a reference to raw
string representation of the directory as represented on disk).
$depth zero corresponds to no extraction of subdirectories (give
C<undef> or an insanely large number to have unlimited depth; e.g.,
1e100).  $fh should be a file handle representing the disk content
with bootsector at $offset.  $o_root is the output directory: the
files in $d will be put there.

If $exists is TRUE, $o_root exists.  (The parent of $o_root should
always exist.)

$how is an optional hash reference, with values for keys C<keep_del>,
C<keep_dots>, C<keep_labels> giving arguments for
interpret_directory() call.

=head2 write_file($fh, $dir, $file, $b, $FAT [, $offset ] )

Extract $file (should be a hash reference representing a record from a
directory) into a directory $dir.  $fh should be a file handle
representing the disk content with bootsector at $offset.

=head1 EXPORT

None by default.

=head1 EXAMPLES

  perl -MFileSystem::LL::FAT=interpret_directory -wle "
    {local $/; binmode STDIN; $s = <STDIN>}
    (undef,@f) = interpret_directory $s, 1;
    print qq($_->{is_subdir} $_->{cluster}\t$_->{size}\t$_->{name}) for @f"
        < dir-clusters

outputs content of a "directory converted to a file" (may be created
by disasterous B<chkdsk> run), including the starting cluster.

Given an information about the number of "pre-cluster sectors", and
size of the cluster, one can convert the starting cluster number to
starting sector number.  Then one can extract the files by raw-read of
the disk partition:

  $sector = $bootsec->{pre_sectors}
          + ($cluster - 2)*$bootsec->{sectors_in_cluster}
	  = $bootsec->{sector_of_cluster0}
          + $cluster * $bootsec->{sectors_in_cluster}

Likewise, one can inspect a bootsector via

  perl -MFileSystem::LL::FAT=interpret_bootsector,check_bootsector -wle
   "{local $/; binmode STDIN; $s = <STDIN>}
    $b = interpret_bootsector $s; check_bootsector $b;
    print qq($_\t=>\t$b->{$_}) for sort keys %$b"
       < disk.bootsector

On DOSish systems one can read bootsector of drive F<d:> by reading
the first 512 bytes of the file F<\\.\d:>.  E.g., with B<dd> one could
do it as

  dd if=//./d: bs=512 count=1 of=disk.bootsector

On UNIXish systems one needs to find the corresponding device file (by
calling B<mount> or B</sbin/mount>?), and do

  dd if=/dev/hda3 bs=512 count=1 of=disk.bootsector

Other DOSish conventions (see also F<diskext>, F<bootpart>, F<mkbt> programs):

  \\?\Device\Harddisk0\Partition0 	# Partition0 is entire disk
  //./physicaldrive0
  /dev/fd0				# Floppy 0 under CygWin
  /dev/sdc				# physical HDs No. 2 (=c) under CygWin
  /dev/sdc1				# Same, partition 1

Other programs may be used too:

  D:\mkbt20>mkbt -x -c e: c:\bootstrap-e2
  * Expert mode (-x)
  * Copy bootsector mode (-c)

  dd if=//./e: of=c:/bootstrap-e-dd count=16
  dd --list

CygWin's C<dd> may be flacky; you may want to try
L<http://www.chrysocome.net/dd>.  You may need I<"elevated privilige">
under Vista.

=head1 BUGS

When lowercasing non-LFN names, which codepage should one use (and how)?

We ignore LFNs records with C<seq-number E<gt> 0x7F>, unless 0xE5.
When do they appear?

How to follow logical partitions?

Test suite is practically absent...

When recursing into a directory without FAT table present, we assume
that subdirs have size of one cluster.  To do otherwise, need to
check that subsequent clusters are not directories; how to do it?

And how often are directories continuous on disk?

=head1 SEE ALSO

See L<http://en.wikipedia.org/wiki/Fat32>.

=head1 AUTHOR

Ilya Zakharevich, E<lt>ilyaz@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2009 by Ilya Zakharevich

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.8 or,
at your option, any later version of Perl 5 you may have available.

=cut