The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
@rem = '--*-Perl-*--
@echo off
if "%OS%" == "Windows_NT" goto WinNT
perl -x -S "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9
goto endofperl
:WinNT
perl -x -S "%0" %*
if NOT "%COMSPEC%" == "%SystemRoot%\system32\cmd.exe" goto endofperl
if %errorlevel% == 9009 echo You do not have Perl in your PATH.
goto endofperl
@rem ';
#!/usr/bin/perl -w
#line 14


use strict;

use Win32API::File 0.04 qw(
    createFile
    fileLastError
    ReadFile
    SetFilePointer
    WriteFile
    CloseHandle
    GetDriveType DRIVE_REMOVABLE DRIVE_FIXED
);

# Get the stripped-down name of this script for use in error messages:
my $Self= $0;
$Self =~ s#^.*[/\\]([^/\\]+)[.][^./\\]*$#$1#;

# Whether to simply overwrite entire boot sector of hard disk:
my $WriteEntire= 0;

exit main();


{
    # Here is a fun way to deal with packed data structures in Perl!
    package Sector0;

    use vars qw( $fmtSect0 @fldSect0 %fldSect0 );
    use vars qw( $fmtFatParams @fldFatParams %fldFatParams );

    BEGIN {
	# Format and field names for an MS-DOS master boot sector [MBR]:
	$fmtSect0= "a3 a8 a429 L S a64 S";
	@fldSect0= qw( pJump sOEM pBoot nSig nZero pPrtns nMarker );
	# Format and field names for part of a FAT partition boot sector [PBR]:
	# [Since pJump, sOEM, and nMarker are the same as for an MBR, we just
	#  extract the fields out of pBoot, even though a PBR doesn't have
	#  nSig, nZero, nor pPrtns.]
	@fldSect0{@fldSect0}= (0..$#fldSect0);
	$fmtFatParams= "S C S C S S C S S S L L a406";
	@fldFatParams=
	  qw( cSectBytes cClustSects cBootSects cFats cDirFiles nMediaType
	      cFatSects cTrackSects cCylHeads cHideSects cDiskSects pFatBoot );
	@fldFatParams{@fldFatParams}= (0..$#fldFatParams);
    }

    sub new
    {
	# Expect the class name and the boot sector data [512-byte string]:
	my( $Self, $pSect0 )= @_;

	# Allow C<$object->new($pSect0)>, not just C<Sector0->new($pSect0)>:
	$Self= ref($Self)   if  ref($Self);

	# Create the hash that will be tied to this package:
	my $tied= {};

	# Let TIEHASH allocate the hash that will hold our object's members:
	my $obj= tie %$tied, $Self;

	# Allow $tied->Meth(), not just $tied->{Mem} and tied(%$tied)->Meth():
	bless $tied, $Self;

	# Populate all of our members:
	$obj->_set_pSect0( $pSect0 );

	# Return the combined ref-to-tied-hash and object:
	return $tied;
    }

    sub TIEHASH
    {
	my( $Self )= @_;
	my $hash= {};
	return bless $hash, $Self;
    }

    # After we modify the pBoot member, we must call this:
    sub _set_pBoot
    {
	my( $obj )= @_;
	$obj->{fldFat} ||= [];	# Don't rely on autovivify.
	@{ $obj->{fldFat} }=  unpack $fmtFatParams, $obj->FETCH("pBoot");
    }

    # Don't store new pSect0 member directly.  Call this instead:
    sub _set_pSect0
    {
	my( $obj, $pSect0 )= @_;
	$obj->{pSect0}= $pSect0;

	$obj->{fldSect0} ||= [];
	@{ $obj->{fldSect0} }= unpack $fmtSect0, $pSect0;

	# The previous line modified the pBoot member:
	$obj->_set_pBoot;

	# All fields are in sync now:
	delete $obj->{bModified};

	#if(  0x55AA != $obj->FETCH("nMarker")  )
    }

    sub FETCH
    {
	my( $obj, $sKey )= @_;
	if(  $obj->{bModified}		# The packed data may be stale:
	and  "pSect0" eq $sKey  ||  "pBoot" eq $sKey  ) {
	    # If requesting packed data, rebuild it from fields first:
	    $obj->STORE(
	      "pBoot",  pack $fmtFatParams, @{ $obj->{fldFat} }  );
	    $obj->{pSect0}= pack $fmtSect0, @{ $obj->{fldSect0} };
	    delete $obj->{bModified};
	}
	if(  "pSect0" eq $sKey  ) {
	    return $obj->{$sKey};
	} elsif(  exists $fldSect0{$sKey}  ) {
	    return $obj->{fldSect0}->[ $fldSect0{$sKey} ];
	} elsif(  exists $fldFatParams{$sKey}  ) {
	    return $obj->{fldFat}->[ $fldFatParams{$sKey} ];
	} else {
	    require Carp;
	    Carp::croak( __PACKAGE__,"::FETCH:  Unknown field ($sKey)" );
	}
    }

    sub STORE
    {
	my( $obj, $sKey, $svNewValue )= @_;
	my $svOldValue;
	if(  "pSect0" eq $sKey  ) {
	    $svOldValue= $obj->{pSect0};
	    $obj->_set_pSect0( $svNewValue );
	    return $svOldValue;
	} elsif(  exists $fldSect0{$sKey}  ) {
	    for(  $obj->{fldSect0}->[ $fldSect0{$sKey} ]  ) {
		$svOldValue= $_;
		$_= $svNewValue;
	    }
	    if(  "pBoot" eq $sKey  ) {
		$obj->_set_pBoot();
	    }
	} elsif(  exists $fldFatParams{$sKey}  ) {
	    for(  $obj->{fldFat}->[ $fldFatParams{$sKey} ]  ) {
		$svOldValue= $_;
		$_= $svNewValue;
	    }
	} else {
	    require Carp;
	    Carp::croak( __PACKAGE__,"::FETCH:  Unknown field ($sKey)" );
	}
	$obj->{Modified}= 1
	  if  $svOldValue ne $svNewValue;
	return $svOldValue;	# This is probably ignored.
    }

    sub FIRSTKEY
    {
	my( $Self )= @_;
	return "pSect0";
    }

    sub NEXTKEY
    {
	my( $Self, $keyPrev )= @_;
	return $fldSect0[0]   if  "pSect0" eq $keyPrev;
	my $idx= $fldSect0{$keyPrev};
	if(  defined $idx  ) {
	    return $fldSect0[1+$idx] || $fldFatParams[0];
	}
	$idx= $fldFatParams{$keyPrev};
	return $fldFatParams[1+$idx] || undef;
    }

}


sub Usage
{
    die
     "Usage:  $Self A: BootFile\n"
    ,"      Copies sector 0 (with boot code) of the A: drive to BootFile.\n"
    ,"   or:  $Self [-e] BootFile A: [Backup]\n"
    ,"      Copies the boot code from BootFile to sector 0 of the A: drive.\n"
    ,"      -e  Overwrites the entire sector 0.  This is the default for\n"
    ,"          floppy disks.  For hard disks, the default is to restore\n"
    ,"          only the boot code [not the signature and partition table].\n"
    ,"      If Backup is given, then the original sector is backed up first.\n"
    ,"A: can also be a number where 0 is the first physical hard disk.\n"
    ;
}


sub BackupBootCode
{
    my( $drive, $file )= @_;
    $drive= "PhysicalDrive".$drive   if  $drive =~ /^\d+$/;
    my $hDrive= createFile( "//./$drive", "r" )
      or  die "Can't read device, $drive: ",fileLastError(),"\n";
    my( $sect0, $cnt );
    ReadFile( $hDrive, $sect0, 512, $cnt, [] )
      or  die "Can't read sector 0 of $drive: ",fileLastError(),"\n";
    die "Read $cnt bytes (not 512) of sector 0 of $drive: ",
      fileLastError(),"\n"
      if  512 != $cnt;
    CloseHandle( $hDrive )
      or  warn "Can't close device, $drive: ",fileLastError(),"\n";
    {
	my $fld= Sector0->new( $sect0 );
	if(  0x55AA != $fld->{nMarker}  ) {
	    warn "Invalid boot sector on $drive (end marker is ",
	      sprintf("0x%X",$fld->{nMarker})," not 0x55AA).\n";
	}
    }
    open( FILE, "> $file\0" )
      or  die "Can't write file, $file: $!\n";
    binmode( FILE )
      or  die "Can't set file output ($file) to binary mode: $!\n";
    print FILE $sect0
      or  die "Can't write sector 0 to file, $file: $!\n";
    close( FILE )
      or  die "Can't close file, $file: $!\n";
}

sub ConfirmContinue
{
    my( $drive )= @_;
    print "Replace ${drive}'s boot sector anyway? ";
    my $resp= <STDIN>;
    if(  $resp !~ /^\s*y/i  ) {
	die "Aborting changes.\n";
    }
}

sub RestoreBootCode
{
    my( $file, $drive )= @_;
    my $bFloppy= 0;
    if(  $drive =~ /^\d+$/  ) {
	$drive= "PhysicalDrive".$drive;
    } else {
	my $type= GetDriveType( $drive );
	if(  DRIVE_REMOVABLE == $type  ) {
	    $bFloppy= 1;
	} elsif(  DRIVE_FIXED != $type  ) {
	    warn "This probably won't work for $drive (type==$type).\n";
	}
    }
    my( $cursect, $newsect, $cnt );
    open( FILE, "< $file\0" )
      or  die "Can't read file, $file: $!\n";
    binmode( FILE )
      or  die "Can't set $file input to binary mode: $!\n";
    $cnt= read( FILE, $newsect, 513 )
      or  die "Can't read sector data from file, $file: $!\n";
    warn "More than 512 bytes in file, $file; ignoring extra.\n"
      if  512 < $cnt;
    die "Fewer than 512 bytes (only $cnt) in file, $file.\n"
      if  $cnt < 512;
    close( FILE )
      or  warn "Can't close file, $file: $!\n";
    my $fldNew= Sector0->new( $newsect );
    if(  0x55AA != $fldNew->{nMarker}  ) {
	warn "Invalid boot sector in file, $file (end marker is ",
	  sprintf("0x%X",$fldNew->{nMarker})," not 0x55AA).\n";
	ConfirmContinue( $drive );
    }
    my $hDrive= createFile( "//./$drive", "rwke" )
      or  die "Can't update device, $drive: ",fileLastError(),"\n";
    ReadFile( $hDrive, $cursect, 512, $cnt, [] )
      or  die "Can't read sector 0 of $drive: ",fileLastError(),"\n";
    die "Read $cnt bytes (not 512) of sector 0 of $drive: ",
      fileLastError(),"\n"
      if  512 != $cnt;
    my $fldCur= Sector0->new( $cursect );
    if(  0x55AA != $fldCur->{nMarker}  ) {
	warn "Invalid boot sector on $drive (end marker is ",
	  sprintf("0x%X",$fldCur->{nMarker})," not 0x55AA).\n";
    }
    if(  $bFloppy  ) {
	# For floppy, verify basic FAT params same then replace entire sector:
	my @dif= map { $fldCur->{$_} ne $fldNew->{$_} } @Sector0::fldFatParams;
	if(  @dif  ) {
	    warn "The following FAT parameters will be changed:\n";
	    for(  @dif  ) {
		warn "\t $_ from $fldCur->{$_} to $fldNew->{$_}\n";
	    }
	    ConfirmContinue( $drive );
	}
    } elsif(  ! $WriteEntire  ) {
	# For hard disk w/o -e, keep old nSig, nZero, pPrtns, and nMarker
	my @keep= qw( nSig nZero pPrtns nMarker );
	@{$fldNew}{@keep}= @{$fldCur}{@keep};
    } # For hard disk w/ -e, replace entire sector.
    $newsect= $fldCur->{pSect0};
    $cnt= SetFilePointer( $hDrive, 0, [], 0 )
      or  die "Can't seek back to front of device, $drive: ",
	    fileLastError(),"\n";
    die "Seeking to front of drive set wrong offset ($cnt).\n"
      if  0 != $cnt;
    WriteFile( $hDrive, $newsect, 512, $cnt, [] )
      or  die "Can't write sector 0 of $drive: ",fileLastError(),"\n";
    die "Write $cnt bytes (not 512) of sector 0 of $drive: ",
      fileLastError(),"\n"
      if  512 != $cnt;
    CloseHandle( $hDrive )
      or  die "Can't close device, $drive: ",fileLastError(),"\n";
}


sub main
{
    @ARGV= map { glob($_) } @ARGV;
    while(  @ARGV  &&  $ARGV[0] =~ /^-/  ) {
	my $opt= shift( @ARGV );
	$opt =~ s/^-//;
	if(  $opt =~ s/^e//i  ) {
	    $WriteEntire= 1;
	} else {
	    die "Unknown option, -$opt.\n";
	}
    }
    die "Current implementation doesn't support three-argument mode.\n"
      if  3 == @ARGV;
    Usage   if  2 != @ARGV;
    my( $src, $dest )= @ARGV;
    if(  $src =~ /^([a-z]:|\d+)$/i  ) {
	die "Can't copy boot code direclty between devices.\n"
	  if  $dest =~ /^([a-z]:|\d+)$/i;
	BackupBootCode( $src, $dest );
    } else {
	RestoreBootCode( $src, $dest );
    }
    return 0;
}

__END__
:endofperl