#!/usr/bin/perl
# $Id$
# spec: repeatedly writes random data to a block device to "shred" it

use strict;
use Fcntl;
use Fcntl ':seek';
use Getopt::Std;
use Time::HiRes;

$| = 1;

my %opts = ();
getopts('b:d:fl:o:p:vx',\%opts);

my $o_buf_size = defined($opts{b}) ? int(eval($opts{b})) : 1024*1024;
my $o_dev_shred = defined($opts{d}) ? $opts{d} : '';
my $o_fork = 0;
my $o_shred_limit = defined($opts{l}) ? int(eval($opts{l})) : 0;
my $o_shred_offset = defined($opts{o}) ? int(eval($opts{o})) : 0;
my $c_pass = defined($opts{p}) ? $opts{p} : 1;
my $o_verbose = $opts{v};
my $o_execute = $opts{x};

if (! -b $o_dev_shred ) { print_help(); die "$0 only knows how to shred devices\n"; }

sub print_help()
{
print <<EOF
$0 - Edoceo, Inc. http://edoceo.com/
Usage:
  -b = buffer size in bytes, eval, 1MiB
  -d = block device to wipe
  -f = fork (not implmented)
  -l = shred limit - only wipe this many bytes, eval
  -o = shred offset - start this many bytes in, eval
  -p = shred count, eval, 1
  -v = verbose
  -x = must be present to execute
  
Any input field marked 'eval' is intened to take perl math expressions

Examples
  Wipe /dev/sdb using default options
    $0 -d /dev/sdb -x

  Wipe only first 2Gib, two passes
    $0 -d /dev/sdb -l 2*1024*1024*1024 -p2 -x

  Wipe 2GiB starting 8GiB in
    $0 -d /dev/sdb -l 2*1024*1024*1024 -o 8*1024*1024*1024 -x

  Wipe Partition/Boot Information (first 512KiB), 500 passes
    $0 -d /dev/sdb -l 512*1024 -p2*250
EOF
}

# Get a buffer full of random data
sub read_random()
{
  print "Reading from /dev/urandom\n";
  my $buf = '';
  sysopen(FH,'/dev/urandom',O_RDONLY) or die $!;
  sysread(FH,$buf,$o_buf_size) or die $!;
  close(FH);
  return $buf;
}

sub size_nice($)
{
  my ($size) = @_;
  my $c = 0;
  my @format = ('B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB');
  while(($size/1024)>1 && $c<scalar(@format)) {
    $size=$size/1024;
    $c++;
  }
  return sprintf('%2.2f%s',$size, $format[$c] );
}

printf("Requested %d pass wipe of %s using %s buffer\n",$c_pass,$o_dev_shred,size_nice($o_buf_size));

my $i_pass = 1;
my $time_x = 0; # Time Checkpoint
# This is supposed to be an array but seems to be a string, split on tv_interval
my $t0 = Time::HiRes::gettimeofday();

for ($i_pass=1;$i_pass<=$c_pass;$i_pass++) {

  my $fh;
  sysopen($fh,$o_dev_shred,O_WRONLY) or die $!;
  my $l_dev_wipe = sysseek($fh,0,SEEK_END);

  # Start wiping at offset
  sysseek($fh,$o_shred_offset,SEEK_SET);

  print "Wiping $o_dev_shred (".size_nice($l_dev_wipe).") ";
  if ($o_shred_offset) { 
    print "starting at offset ".size_nice($o_shred_offset)." ";
  }
  print "- pass $i_pass/$c_pass\n";

  if (!$o_execute) {
    print "Skipping wipe. Use '-x' to execute.\n";
    next;
  }

  my $buf = read_random();

  
  # @todo Implement Alarm in place of this shit
  my $i = 0;
  my $t0 = [Time::HiRes::gettimeofday()];
  
  while (1) {
    $i++;

    my $bw = syswrite($fh,$buf);
    if ($bw < $o_buf_size) {
      # todo: make this smarter
      print "\nFailed to write $o_buf_size bytes; return = $bw\n";
      if (eof($fh)) {
        print "End of Device\n";
        last;
      }
      $i = 0;
      last;
    }
    
    # Give an update every 10 chunks written
    # Write out Every one Second
    my $time_c = Time::HiRes::tv_interval($t0);
    if ( $time_c >= ($time_x + 5) ) {
      my $bytes_w = $i * $o_buf_size;
      # my $time_c = Time::HiRes::tv_interval($t0);
      # Number of minutes remaining
      my $time_r = (($l_dev_wipe - $bytes_w) / ($bytes_w / $time_c)) / 60;
      my $time_x = $time_c;
      printf("Shredded %s of %s in %.2fs (%s/s) - ETA:  %.2fm        \r",
        size_nice($bytes_w),
        size_nice($l_dev_wipe),
        $time_c,
        size_nice($bytes_w / $time_c),
        $time_r);
    }
  }
  
  close($fh);
}

# printf("Shredded %s of %s in %.2fs - ETA:  %.2fm        \n",size_nice($bytes_w),size_nice($l_dev_wipe),$time_c,$time_r);
