#!/usr/bin/perl
#
# __copy1__
# __copy2__
#
# read/write data like 'dd', but showing timings, usefull for
# test and benchmarks
#
use warnings;
use strict;

our $CMD		= "ddtime";
our $CMDVER	= "2.3";
our $CMDSTR	= "$CMD v$CMDVER (2026-01-13)";

our $read;
our $write;
our $buf;
our $stime;	# start time
our $ctime;	# current time
our $ptime;	# previous time
our $tot_read	= 0;
our $prog_read	= 0;
our $maxbytes	= 0;
our $maxblocks	= 0;
our $blksize	= 512;
our $blocks_read	= 0;
our $pattern	= "";
our $OUTFILE	= "";
our $sync_cnt	= 32;

while (@ARGV) {
	$_	= shift;
	if ($_ =~ /^bs=/) {
		$blksize	= translate_size($_);
		die( "\n$CMD: blksize cannot be zero\n" )	if (!$blksize);
		next;
	}
	if ($_ =~ /^count=/) {
		$maxblocks	= $_;
		$maxblocks	=~ s/^count=//;
		die( "\n$CMD: count must be a number\n" )	if ($maxblocks !~ /^\d+$/);
		next;
	}
	if ($_ =~ /^max=/) {
		$maxbytes	= translate_size($_);
		next;
	}
	if ($_ =~ /^pattern=/) {
		$pattern	= $_;
		next;
	}
	if ($_ =~ /^of=/) {
		$OUTFILE	= $_;
		$OUTFILE	=~ s/^of=//;
		next;
	}
	if ($_ =~ /^sync=/) {
		$sync_cnt	= $_;
		$sync_cnt	=~ s/^sync=//;
		die( "\n$CMD: sync must be a number\n" )	if ($sync_cnt !~ /^\d+$/);
		next;
	}
	usage();
}


# sanity checks

if ($maxbytes) {
	die( "\n$CMD error: read limit ($maxbytes) cannot be less than blksize ($blksize)\n" )
		if ($maxbytes < $blksize);
	printf( STDERR "\n $CMD: read limit set to %d bytes\n\n", $maxbytes );
} else {
	if ($maxblocks) {
		$maxbytes	= $maxblocks * $blksize;
		printf( STDERR "\n $CMD: read limit set to %d bytes (%d blocks)\n\n", $maxbytes, $maxblocks );
	}
}

if ($OUTFILE) {
	if ($OUTFILE =~ /^\/dev\// && ! -e $OUTFILE) {
		die( "\n$CMD error: device '$OUTFILE' not exists\n" );
	}
	open( OUTFILE, ">$OUTFILE" ) or die( "\n$CMD error: can't open '$OUTFILE': $!\n" );
}


fill_pattern( $pattern )	if ($pattern);


# start data copy
#
$stime	= time();
$ptime	= $stime;
$read	= $blksize;


while (1) {

	if ($pattern eq "") {
		$read = sysread( STDIN, $buf, $blksize );
		if (!defined $read) {
			report();
			die( "\n$CMD: read error: $!\n" );
		}
		if ($read == 0) {
			report();
			exit( 0 );
		}
	}

	if ($OUTFILE) {
		$write	= syswrite( OUTFILE, $buf, $read );
	} else {
		$write	= syswrite( STDOUT, $buf, $read );
	}
	if (!defined $write) {
		report();
		die( "\n$CMD: error writing: $!\n" );
	}
	if ($write != $read) {
		report();
		die( "\n$CMD: error writing, short (read=$read, write=$write)\n" );
	}
	$tot_read	+= $read;
	$prog_read	+= $read;
	$blocks_read	++;

	# if outfile defined, sync every $sync_cnt blocks
	#
	sync_out()	if ( !($blocks_read % $sync_cnt) );

	last		if ($maxbytes && $tot_read >= $maxbytes);

	$ctime	= time();
	if ($ctime != $ptime) {
		print_stats();
		$prog_read	= 0;
		$ptime		= $ctime;
	}
}

if ($OUTFILE) {
	sync_out();
	close( OUTFILE )	or die( "\n$CMD error writing on '$OUTFILE': $!\n" );
}

report();
print( STDERR "\n" );
exit( 0 );




sub usage {
	printf( STDERR "
== %s == copy data showing I/O stats and progress ==

usage:	$CMD [options] <input >output
	$CMD [options] of=output [sync=nnn] <input
	$CMD [options] pattern=type of=output [sync=nnn]

options: bs=blocksize count=nnn max=size

where:
  . sizes can be provided with units, like '32k'
  . use count (blocks count) or max (byes written) to limit
    the total amount of data transferred
  . pattern generates internally the output data (never reads),
    available patterns are:

      r|random	use a repeated random block (from /dev/urandom)
      0|zero	use all 0 bits
      1|one	use all 1 bits

caveats:
  . block counts are messed up if you read from a character buffered
    input, like a tty
  . since read are always performed using 'blocksize' buffer, max=size
    that are not blocksize multiple will results in more bytes read
    than expected
",
	$CMDSTR );
	exit(1);
}


sub translate_size {
	my ($isize)	= @_;
	   $isize	=~ s/^[a-z]*=//;
	my $osize	= $isize;

	CASE: {
		if ($isize =~ /^\d+$/) {
			last CASE;
		}
		if ($isize =~ /[kK][bB]*$/) {
			$osize	=~ s/[a-zA-Z].*//;
			$osize	*= 1024;
			last CASE;
		}
		if ($isize =~ /[mM][bB]*$/) {
			$osize	=~ s/[a-zA-Z].*//;
			$osize	*= 1048576;
			last CASE;
		}
		if ($isize =~ /[gG][bB]*$/) {;
			$osize	=~ s/[a-zA-Z].*//;
			$osize	*= 1073741824;
			last CASE;
		}
		printf( STDERR "%s: wrong size: %s (use k, m, g, Kb, Mb, Gb)\n", $CMD, $isize );
		exit( 1 );
	}
	return $osize;
}


sub report {
	$ctime	= time();
	$ctime	= $stime + 1	if ($ctime <= $stime);
	my $tks	= $tot_read / ($ctime - $stime);
	printf( STDERR
			"%s $CMD: %-10s   written %-10s (%d %s-blocks) at %s/sec\n",
			"\r\033[K",
			fmttime($ctime - $stime),
			fmtsize($tot_read,1),
			$blocks_read, fmtsize($blksize,0),
			fmtsize( $tks,1 ),
		);
	##printf STDERR ">>>stime=$stime ctime=$ctime\n";
}

sub fmtsize {
	my ($bytes,$dec)	= @_;
	$dec			= 3	if (!defined $dec);

	my $k	= $bytes / 1024;
	my $m	= $k / 1024;
	my $g	= $m / 1024;

	return sprintf( "%.${dec}f Gb", $g )		if ( int($g) );
	return sprintf( "%.${dec}f Mb", $m )		if ( int($m) );
	return sprintf( "%.${dec}f Kb", $k )		if ( int($k) );
	return sprintf( "%d bytes", $bytes );
}


sub fmttime {
	my ($time)	= @_;
	
	my $h	= int( $time / 3600 );
	my $m	= int( ( $time - $h * 3600 ) / 60 );
	my $s	= $time - $h * 3600 - $m * 60;

	return sprintf( "%d:%02d:%02d", $h, $m, $s )	if ($h);
	return sprintf( "%2d:%02d", $m, $s )		if ($m);
	return sprintf( "%d secs", $s );
}


sub fill_pattern
{
	my ($type)	= @_;

	$type	=~ s/^pattern=//;

	printf( STDERR "\r\033[K $CMD: filling pattern buffer ...\r" );

	if ($type =~ /^r/) {
		$buf	= `dd if=/dev/urandom bs=$blksize count=1 2>/dev/null`;
		return;
	}
	if ($type =~ /^1|^o/) {
		$buf	= sprintf( "%c", 255 );
		$buf	= $buf x $blksize;
		return;
	}
	if ($type =~ /^0|^z/) {
		$buf	= sprintf( "%c", 0 );
		$buf	= $buf x $blksize;
		return;
	}

	print( STDERR " $CMD error: unknown type '$type'\n" );
	usage();
}

sub sync_out
{
	return if (!$OUTFILE);

	$ctime	= time();
	print_stats( "*sync*" );
	system( "/bin/sync", $OUTFILE );
	print_stats();
	return 1;
}



sub print_stats
{
	my ($msg) = @_;
	my $kps	= 0;
	my $tks	= 0;

	$kps	= $prog_read / ($ctime - $ptime)	if ($ctime - $ptime);
	$tks	= $tot_read / ($ctime - $stime)		if ($ctime - $stime);

	$msg = ""	if (!defined $msg);

	printf( STDERR
	    "\r\033[K %s blk=%d   %s   %s/sec   (avg %s/sec)  %s\r",
	    fmttime($ctime - $stime),
	    $blocks_read,
	    fmtsize($tot_read,1),
	    fmtsize($kps,1),
	    fmtsize($tks,1),
	    $msg,
	);
	return 1;
}
