From d1c3433353fc09ca56ff076daaa33b2741ade505 Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Fri, 30 Mar 2018 23:44:08 +0200 Subject: [PATCH] Add rw(1). --- Makefile | 1 + rw/.gitignore | 1 + rw/Makefile | 28 ++ rw/rw.1 | 453 ++++++++++++++++++++ rw/rw.c | 869 ++++++++++++++++++++++++++++++++++++++ utils/command-not-found.c | 8 + 6 files changed, 1360 insertions(+) create mode 100644 rw/.gitignore create mode 100644 rw/Makefile create mode 100644 rw/rw.1 create mode 100644 rw/rw.c diff --git a/Makefile b/Makefile index 5e058a10..c06d82a5 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ kblayout-compiler \ login \ mkinitrd \ regress \ +rw \ sf \ sh \ sysinstall \ diff --git a/rw/.gitignore b/rw/.gitignore new file mode 100644 index 00000000..16feab09 --- /dev/null +++ b/rw/.gitignore @@ -0,0 +1 @@ +rw diff --git a/rw/Makefile b/rw/Makefile new file mode 100644 index 00000000..2e19fe8c --- /dev/null +++ b/rw/Makefile @@ -0,0 +1,28 @@ +include ../build-aux/platform.mak +include ../build-aux/compiler.mak +include ../build-aux/version.mak +include ../build-aux/dirs.mak + +OPTLEVEL?=$(DEFAULT_OPTLEVEL) +CFLAGS?=$(OPTLEVEL) + +CFLAGS += -Wall -Wextra + +BINARIES = rw +MANPAGES1 = rw.1 + +all: $(BINARIES) + +.PHONY: all install clean + +install: all + mkdir -p $(DESTDIR)$(BINDIR) + install $(BINARIES) $(DESTDIR)$(BINDIR) + mkdir -p $(DESTDIR)$(MANDIR)/man1 + cp $(MANPAGES1) $(DESTDIR)$(MANDIR)/man1 + +%: %.c + $(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) $< -o $@ + +clean: + rm -f $(BINARIES) diff --git a/rw/rw.1 b/rw/rw.1 new file mode 100644 index 00000000..a8aeff84 --- /dev/null +++ b/rw/rw.1 @@ -0,0 +1,453 @@ +.Dd March 6, 2018 +.Dt RW 1 +.Os +.Sh NAME +.Nm rw +.Nd blockwise input/output +.Sh SYNOPSIS +.Nm +.Op Fl afhPstvx +.Op Fl b Ar block-size +.Op Fl c Ar count +.Op Fl I Ar input-offset +.Op Fl i Ar input-file +.Op Fl O Ar output-offset +.Op Fl o Ar output-file +.Op Fl p Ar interval +.Op Fl r Ar input-block-size +.Op Fl w Ar output-block-size +.Sh DESCRIPTION +.Nm +reads blocks from the standard input and copies them to the standard output +until the end of the standard input. +The input block size and output block sizes default to appropriate values for +efficiently reading from the input and writing to the output. +Input and output will be done as aligned as possible on the block size +boundaries. +The final input block can be partial. +Output blocks are written whenever enough input blocks have been read, or +partially written whenever the end of the input is reached. +The output file is not truncated after the copy is done. +.Pp +Byte quantities can be specified as a non-negative count of bytes in decimal +format, hexadecimal format (with leading +.Sy 0x ) , +or octal format (with leading +.Sy 0 ) ; +optionally followed by any amount of whitespace, and then suffixed with any of +the following case-insensitive suffixes that multiplies the specified quantity +by that magnitude: +.Pp +.Bl -tag -width "12345678" -compact +.It Sy B +Magnitude of a byte (1 byte). +.It Sy K , Sy KiB +Magnitude of kibibytes (1,024 bytes). +.It Sy M , Sy MiB +Magnitude of mebibytes (1,048,576 bytes). +.It Sy G , Sy GiB +Magnitude of gibibytes (1,073,741,824 bytes). +.It Sy T , Sy TiB +Magnitude of tebibytes (1,099,511,627,776 bytes). +.It Sy P , Sy PiB +Magnitude of pebibytes (1,125,899,906,842,624 bytes). +.It Sy E , Sy EiB +Magnitude of exbibytes (1,152,921,504,606,846,976 bytes). +.It Sy r +Magnitude of input blocks (the default input block size when setting block +sizes). +.It Sy w +Magnitude of output blocks (the default output block size when setting block +sizes +.It Sy x +Magnitude of input and output blocks (if they have the same size) (the default +block size when setting block sizes). +.El +.Pp +The options are as follows: +.Bl -tag -width "12345678" +.It Fl a +In combination with +.Fl o , +open the output file in append mode. +This option must be used with +.Fl o +and is incompatible with +.Fl t . +The output offset is set to the size of the output file. +.It Fl b Ar block-size +Set both the input and output block sizes to +.Ar block-size . +.It Fl c Ar count +Stop after copying the byte quantify specified by +.Ar count , +in addition to stopping when the end of the input is reached. +If the +.Ar count +starts with a leading +.Sq - , +stop that many bytes before the end of the input (only works if the input size +is known). +.It Fl f +Continue as much as possible in the event of I/O errors and exit unsuccessfully +afterwards. +For each input failure, skip forward to the next input block or the next +native-size input block or the end of the file (whichever comes first), write an +error to the standard error, and replace the failed input with NUL bytes when +writing it to the output. +For each output failure, skip forward to the next output block or the next +native-size output block (whichever comes first) and write an error to the +standard error. +The native-size input and output blocks are those defined by the preferred +input/output size for the input and output. +This option only works for the input if it is seekable and the input size is +known. +This option only works for the output if it is seekable (and +.Fl a +is not set). +Beware that the default preferred input/output sizes may be larger than the +underlying storage sector sizes: If this option is used, the +.Fl r +and +.Fl w +options should be set to the appropriate input/output sector sizes, or more than +just the bad sector may be skipped. +.It Fl h +Write statistics in the human readable format where byte amounts and time +amounts are formatted according to their magnitude as described in the +.Sx DIAGNOSTICS +section. +.It Fl I Ar offset +Skip past the first +.Ar offset +bytes in the input before the copying begins. +If the +.Ar offset +starts with a leading +.Sq - +it is interpreted as that many bytes before the end of the input (if the size is +known), and if it starts with a leading +.Sq + +it is interpreted as that many bytes after the end of the input (if the size is +known). +If the input is not seekable, the first +.Ar offset +bytes are read and discarded before the copying begins. +If the +.Ar offset +is not a multiple of the input block size, the first input block is reduced in +size such that it ends at a input-block-size-aligned position in the input. +.It Fl i Ar input-file +Read the input from +.Ar input-file +instead of the standard input. +.It Fl O Ar offset +Seek past the first +.Ar offset +bytes in the output before the copying begins. +If the +.Ar offset +starts with a leading +.Sq - +it is interpreted as that many bytes before the end of the output (if the size +is known), and if it starts with a leading +.Sq + +it is interpreted as that many bytes after the end of the output (if the size +is +known). +If the output is not seekable, the number of NUL bytes specified in +.Ar offset +are written to the output before the copying begins. +This option cannot be set to a non-zero value if +.Fl a +is set. +If the +.Ar offset +is not a multiple of the output block size, the first output block is reduced in +size such that it ends at a output block size aligned position in the output. +.It Fl o Ar output-file +Write the output to +.Ar output-file +instead of the standard output, creating the file if it doesn't exist. +If +.Ar output-file +already exists, the existing data is not discarded. +Use +.Fl t +if you want to truncate the output afterwards. +.It Fl P +Pad the final output block with NUL bytes, such that the final output offset +(counting the initial offset with +.Fl O ) +is a multiple of the output block size. +.It Fl p Ar interval +Write occasional statistics to the standard error during the transfer and on +completion, or when being terminated by +.Dv SIGINT . +.Ar interval +is a non-negative integer of how long the interval is in whole seconds between +statistics being written. +Statistics are written for every read and write if the +.Ar interval +is zero. +The format is described in the +.Sx DIAGNOSTICS +section. +.It Fl r Ar input-block-size +Set the input block size to +.Ar input-block-size . +.It Fl s +Sync the output on successful completion. +.It Fl t +Truncate the output to the final output position after the copy has completed. +This option requires the output to be truncatable. +This option is incompatible with +.Fl a . +.It Fl v +Write statistics to the standard error upon completion, or when being terminated +by +.Dv SIGINT . +The format is described in the +.Sx DIAGNOSTICS +section. +.It Fl w Ar output-block-size +Set the output block size to +.Ar output-block-size . +.It Fl x +In combination with +.Fl o , +fail if the output file already exists. +.El +.Sh ASYNCHRONOUS EVENTS +.Bl -tag -width "SIGUSR1" +.It Dv SIGINT +If +.Fl v +or +.Fl p +is set, abort the copy, write statistics to the standard error, and then exit as +if killed by +.Dv SIGINT . +.It Dv SIGUSR1 +Write statistics to the standard error and continue the copy. +If +.Dv SIGUSR1 +is not ignored, this handler is installed and this signal is unblocked. +To use this signal without a race condition before the signal handler is +installed (as +.Dv SIGUSR1 +is deadly by default), block the signal before loading this program. +To disable the handling of this signal, ignore the signal before loading this +program. +.El +.Sh EXIT STATUS +.Nm +will exit 0 on success and non-zero otherwise. +.Sh EXAMPLES +Copy from the standard input to the standard output: +.Bd -literal +rw +.Ed +.Pp +Copy the first 256 bytes from the input to the output: +.Bd -literal +rw -c 256 +.Ed +.Pp +Copy from the input file +.Pa foo +to the beginning of the output file +.Pa bar +(preserving any data in the output file beyond the final output position after +the copy is finished). +.Bd -literal +rw -i foo -o bar +.Ed +.Pp +Copy from the input file to the beginning of the output file, truncating the +output file to the final output position afterwards: +.Bd -literal +rw -i foo -o bar -t +.Ed +.Pp +Copy from the input file +.Pa foo +to the beginning of the output block device +.Pa /dev/bar +(preserving any existing data on the output block device beyond the copied +area), while writing progress statistics every 10 seconds in the human readable +format, and sync the output block device afterwards: +.Bd -literal +rw -i foo -o /dev/bar -p 10 -h -s +.Ed +.Pp +Skip the first 512 bytes of the input, and then append the next 1024 bytes to +the output file +.Pa bar : +.Bd -literal +rw -I 512 -c 1024 -o bar -a +.Ed +.Pp +Copy 2 KiB from offset 768 in the input file +.Pa foo +to offset 256 MiB in the output file +.Pa bar . +.Bd -literal +rw -c 2K -i foo -I 768 -o bar -O 256M +.Ed +.Pp +Copy from sector 32 and 4 sectors onwards from a block device +.Pa /dev/foo +(with the sector size being 512 bytes) +to the output file +.Pa bar : +.Bd -literal +rw -r 512 -i /dev/foo -I 32r -c 4r -o bar +.Ed +.Pp +With a block size of 4096 bytes, copy 64 blocks from the input from offset 32 +blocks in the input to offset 65536 blocks in the output: +.Bd -literal +rw -b 4096 -c 64x -I 32x -O 65536x +.Ed +.Pp +Back up the +.Pa /dev/foo +block device (with the sector size being 512 bytes) to the +.Pa bar +output file, continuing despite I/O errors by writing error messages to the +standard error and writing NUL bytes to the output instead, truncating the +output file to the size of the input, writing progress statistics every 10 +seconds in the human readable format to the standard error: +.Bd -literal +rw -f -i /dev/foo -r 512 -o bar -t -p 10 -h +.Ed +.Pp +With the input block size of 512 bytes and the output block size of 8192 bytes, +copy 16384 input blocks from input block 65536 onwards to output block 1048576: +.Bd -literal +rw -r 512 -w 8192 -c 16384r -I 65536r -O 1048576w +.Ed +.Pp +Copy 512 bytes from 1024 bytes before the end of the input to 2048 bytes after +the current size of the output file: +.Bd -literal +rw -c 512 -I -1024 -o bar -O +2048 +.Ed +.Pp +Skip the first 100 bytes of the input and copy until 200 bytes are left in the +input file: +.Bd -literal +rw -i foo -I 100 -c -200 +.Ed +.Sh DIAGNOSTICS +Statistics about the copy are written to the standard error upon completion +if either +.Fl v +or +.Fl p +are set; occasionally if +.Fl p +is set; upon +.Dv SIGINT +(if not ignored when the program was loaded) if +.Fl v +is set; and upon +.Dv SIGUSR1 +(if not ignored when the program was loaded). +.Pp +The statistics are in this format: +.Bd -literal + s B / B % B/s s +.Ed +.Pp +.Ar time-elapsed +is the number of seconds since the copying began. +.Ar done +is the number of bytes copied so far. +.Ar total +is an estimate of how many bytes will be copied, or +.Sq "?" +if not known. +.Ar percent +is how many percent complete the copy is, or +.Sq "?" +if not known. +.Ar speed +the average speed of copying so far in bytes per second, or +.Sq "?" +if it is too early to tell. +.Ar time-left +is the number of seconds left, assuming the remaining data is copied at the +current average speed, or +.Sq "?" +is not known. +.Pp +For instance, the statistics could look like this: +.Bd -literal +7 s 714682368 B / 1238364160 B 57% 102097481 B/s 5 s +.Ed +.Pp +The statistics are printed with human readable byte units (B, KiB, MiB, GiB, +TiB, PiB, EiB) and time units (s, m, h, d) if the +.Fl h +option is set: +.Bd -literal +7 s 714.4 MiB / 1.1 GiB 60% 102.0 MiB/s 4 s +.Ed +.Sh SEE ALSO +.Xr cat 1 , +.Xr cp 1 , +.Xr dd 1 +.Sh HISTORY +.Nm +originally appeared in Sortix 1.1. +.Pp +.Nm +is similar to +.Xr dd 1 , +but has a distinct design and improvements: +.Bl -bullet +.It +The command line options use the conventional option format. +.It +The output file is not truncated by default. +One has to use +.Fl t . +.It +The input and output block sizes default to the preferred I/O block sizes +instead of 512 bytes. +.Pp +The +.Fl c , I , +and +.Fl O +options accept byte quantities by default instead of block counts, but can +be specified in block counts by using the +.Sq r , w , +and +.Sq x +suffixes. +.It +Statistics are not written by default. +One has to use +.Fl v +or +.Fl p . +The statistics contain more useful information and is machine readable as it +contains no localized information. +A human readable statistics format is available using +.Fl h . +Statistics can occasionally be written out using +.Fl p . +.It +There is no support for converting ASCII to EBCDIC, converting ASCII to a +different EBCDIC, EBCDIC to ASCII, swapping pairs of bytes, converting the bytes +to lower-case or upper-case, converting line-delimited data into fixed-size +blocks, or converting fixed-sized blocks into line-delimited data. +.It +Offsets can be specified relative to the end of the input/output. +.It +Input errors stop the copying immediately rather than writing out a partial +output block. +.El diff --git a/rw/rw.c b/rw/rw.c new file mode 100644 index 00000000..c16d671a --- /dev/null +++ b/rw/rw.c @@ -0,0 +1,869 @@ +/* + * Copyright (c) 2016, 2017, 2018 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * rw.c + * Blockwise input/output. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef OFF_MAX +#define OFF_MAX ((off_t) ((UINTMAX_C(1) << (sizeof(off_t) * 8 - 1)) - 1)) +#endif + +static uintmax_t parse_quantity(const char* string, + blksize_t input_blksize, + blksize_t output_blksize) +{ + const char* end; + if ( *string < '0' || '9' < *string ) + errx(1, "invalid quantity: %s", string); + errno = 0; + uintmax_t value = strtoumax(string, (char**) &end, 0); + if ( value == UINTMAX_MAX && errno == ERANGE ) + errx(1, "argument overflow: %s", string); + if ( *end ) + { + while ( isspace((unsigned char) *end) ) + end++; + uintmax_t magnitude = 1; + const char* unit = end; + unsigned char magc = tolower((unsigned char) *end); + switch ( magc ) + { + case '\0': errx(1, "trailing whitespace in quantity: %s", string); + case 'b': magnitude = 1; break; + case 'k': magnitude = UINTMAX_C(1024) << (0 * 10); break; + case 'm': magnitude = UINTMAX_C(1024) << (1 * 10); break; + case 'g': magnitude = UINTMAX_C(1024) << (2 * 10); break; + case 't': magnitude = UINTMAX_C(1024) << (3 * 10); break; + case 'p': magnitude = UINTMAX_C(1024) << (4 * 10); break; + case 'e': magnitude = UINTMAX_C(1024) << (5 * 10); break; + case 'r': magnitude = input_blksize; break; + case 'w': magnitude = output_blksize; break; + case 'x': + if ( input_blksize != output_blksize ) + errx(1, "input block size is not output block size: %s", + string); + magnitude = input_blksize; + break; + default: errx(1, "unsupported unit: %s", unit); + } + end++; + if ( (tolower(magc) != 'b' && + tolower(magc) != 'r' && + tolower(magc) != 'w' && + tolower(magc) != 'x') && + strcasecmp(end, "iB") == 0 ) + end += 2; + if ( *end != '\0' ) + errx(1, "unsupported unit: %s", unit); + if ( magnitude != 0 && UINTMAX_MAX / magnitude < value ) + errx(1, "argument overflow: %s", string); + value *= magnitude; + } + return value; +} + +static size_t parse_size_t(const char* string, + blksize_t input_blksize, + blksize_t output_blksize) +{ + uintmax_t result = parse_quantity(string, input_blksize, output_blksize); + if ( result != (size_t) result || SSIZE_MAX < result ) + errx(1, "argument overflow: %s", string); + return (size_t) result; +} + +static off_t parse_off_t(const char* string, + blksize_t input_blksize, + blksize_t output_blksize) +{ + uintmax_t result = parse_quantity(string, input_blksize, output_blksize); + if ( result != (uintmax_t) (off_t) result ) + errx(1, "argument overflow: %s", string); + return (off_t) result; +} + +static off_t parse_offset(const char* string, + blksize_t input_blksize, + blksize_t output_blksize, + off_t size) +{ + if ( string[0] == '-' ) + { + off_t result = parse_off_t(string + 1, input_blksize, output_blksize); + if ( size < result ) + errx(1, "value smaller than file size: %s", string); + return size - result; + } + else if ( string[0] == '+' ) + { + off_t result = parse_off_t(string + 1, input_blksize, output_blksize); + if ( OFF_MAX - size < result ) + errx(1, "argument overflow: %s", string); + return size + result; + } + return parse_off_t(string, input_blksize, output_blksize); +} + +static time_t parse_time_t(const char* string) +{ + const char* end; + if ( *string < '0' || '9' < *string ) + errx(1, "invalid duration: %s", string); + errno = 0; + uintmax_t value = strtoumax(string, (char**) &end, 0); + if ( value == UINTMAX_MAX && errno == ERANGE ) + errx(1, "argument overflow: %s", string); + if ( *end ) + errx(1, "invalid duration: %s", string); + if ( value != (uintmax_t) (time_t) value ) + errx(1, "argument overflow: %s", string); + return (time_t) value; +} + +static time_t timediff(struct timespec now, struct timespec then) +{ + time_t result = now.tv_sec - then.tv_sec; + if ( now.tv_nsec < then.tv_nsec ) + result--; + return result; +} + +static int percent_done(off_t done, off_t total) +{ + if ( total < 0 || total < done ) + return -1; + // Avoid overflow when multiplying by 100 by reducing the problem. + if ( OFF_MAX / 65536 <= done ) + { + done /= 65536; + total /= 65536; + } + if ( total == 0 ) + return 100; + return (done * 100) / total; +} + +static void format_bytes_amount(char* buf, + size_t len, + uintmax_t value, + bool human_readable) +{ + if ( !human_readable ) + { + snprintf(buf, len, "%ju B", value); + return; + } + uintmax_t value_fraction = 0; + uintmax_t exponent = 1024; + char suffixes[] = { 'B', 'K', 'M', 'G', 'T', 'P', 'E' }; + size_t num_suffixes = sizeof(suffixes) / sizeof(suffixes[0]); + size_t suffix_index = 0; + while ( exponent <= value && suffix_index + 1 < num_suffixes) + { + value_fraction = value % exponent; + value /= exponent; + suffix_index++; + } + char suffix_str[] = + { suffixes[suffix_index], 0 < suffix_index ? 'i' : '\0', 'B', '\0' }; + char fraction_char = '0' + (value_fraction / (1024 / 10 + 1)) % 10; + snprintf(buf, len, "%ju.%c %s", value, fraction_char, suffix_str); +} + +static void format_time_amount(char* buf, + size_t len, + uintmax_t value, + bool human_readable) +{ + if ( !human_readable || value < 60 ) + snprintf(buf, len, "%ju s", value); + else if ( value < 60 * 60 ) + { + int seconds = value % 60; + int fraction = (seconds * 10) / 60; + uintmax_t minutes = value / 60; + snprintf(buf, len, "%ju.%i m", minutes, fraction); + } + else if ( value < 24 * 60 * 60 ) + { + int minutes = (value / 60) % 60; + int fraction = (minutes * 10) / 60; + uintmax_t hours = value / (60 * 60); + snprintf(buf, len, "%ju.%i h", hours, fraction); + } + else + { + int minutes = (value / 60) % (24 * 60); + int fraction = (minutes * 10) / (24 * 60); + uintmax_t days = value / (24 * 60 * 60); + snprintf(buf, len, "%ju.%i d", days, fraction); + } +} + +static volatile sig_atomic_t signaled = 0; +static volatile sig_atomic_t interrupted = 0; + +static void on_signal(int signum) +{ + signaled = 1; + if ( signum == SIGINT ) + interrupted = 1; +} + +static void progress(struct timespec start, + off_t done, + off_t total, + bool human_readable, + struct timespec* last_statistic, + time_t interval) +{ + // Write statistics if signaled or if an interval has been set with -p. + bool handling_signal = signaled || interrupted; + struct timespec now; + if ( !handling_signal ) + { + if ( interval < 0 ) + return; + clock_gettime(CLOCK_MONOTONIC, &now); + if ( 0 < interval ) + { + time_t since_last = timediff(now, *last_statistic); + if ( since_last <= 0 ) + return; + last_statistic->tv_sec += interval; + } + } + // Avoid system calls being interrupted when writing statistics, but ensure + // that we die by SIGINT if it happens for the second twice. + sigset_t sigset, oldsigset; + sigemptyset(&sigset); + sigaddset(&sigset, SIGUSR1); + if ( !interrupted ) + sigaddset(&sigset, SIGINT); + sigprocmask(SIG_BLOCK, &sigset, &oldsigset); + if ( handling_signal ) + clock_gettime(CLOCK_MONOTONIC, &now); + time_t duration = timediff(now, start); + int percent = percent_done(done, total); + off_t speed = -1; + if ( 0 < duration ) + speed = done / duration; + time_t countdown = -1; + if ( 0 < speed && 0 <= total && done <= total ) + { + off_t countdown_off = (total - done) / speed; + if ( (time_t) countdown_off == countdown_off ) + countdown = countdown_off; + } + char duration_str[3 * sizeof(duration) + 2]; + format_time_amount(duration_str, sizeof(duration_str), duration, + human_readable); + char done_str[3 * sizeof(done) + 2]; + format_bytes_amount(done_str, sizeof(done_str), done, human_readable); + char total_str[3 * sizeof(total) + 2] = "? B"; + if ( 0 <= total ) + format_bytes_amount(total_str, sizeof(total_str), total, + human_readable); + char percent_str[5] = "?%"; + if ( 0 <= percent ) + snprintf(percent_str, sizeof(percent_str), "%i%%", percent); + char speed_str[3 * sizeof(speed) + 2] = "? B"; + if ( 0 <= speed ) + format_bytes_amount(speed_str, sizeof(speed_str), speed, + human_readable); + char countdown_str[3 * sizeof(countdown) + 2] = "? s"; + if ( 0 <= countdown ) + format_time_amount(countdown_str, sizeof(countdown_str), countdown, + human_readable); + fprintf(stderr, "%s %s / %s %s %s/s %s\n", + duration_str, + done_str, + total_str, + percent_str, + speed_str, + countdown_str); + if ( interrupted ) + raise(SIGINT); + if ( handling_signal ) + signaled = 0; + sigprocmask(SIG_SETMASK, &oldsigset, NULL); +} + +int main(int argc, char *argv[]) +{ + // SIGUSR1 is deadly by default until a handler is installed, let users + // avoid the race condition by letting them block if before loading this + // program and then it's unblocked after a handler is installed. Allow + // disabling SIGUSR1 handling by setting the handler to ignore before + // loading this program. + struct sigaction sa; + sigaction(SIGUSR1, NULL, &sa); + bool handle_sigusr1 = sa.sa_handler != SIG_IGN; + if ( handle_sigusr1 ) + { + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_signal; + sa.sa_flags = 0; // Don't restart system calls. + sigaction(SIGUSR1, &sa, NULL); + sigset_t usr1_set; + sigemptyset(&usr1_set); + sigaddset(&usr1_set, SIGUSR1); + sigset_t old_sigset; + sigprocmask(SIG_UNBLOCK, &usr1_set, &old_sigset); + } + + bool append = false; + bool force = false; + bool human_readable = false; + bool no_create = false; + bool pad = false; + bool sync = false; + bool truncate = false; + bool verbose = false; + const char* count_str = NULL; + const char* input_path = NULL; + const char* output_path = NULL; + const char* input_blksize_str = NULL; + const char* output_blksize_str = NULL; + const char* input_offset_str = NULL; + const char* output_offset_str = NULL; + const char* progress_str = NULL; + + int opt; + while ( (opt = getopt(argc, argv, "ab:c:fhI:i:O:o:Pp:r:stvw:x")) != -1 ) + { + switch ( opt ) + { + case 'a': append = true; break; + case 'b': input_blksize_str = output_blksize_str = optarg; break; + case 'c': count_str = optarg; break; + case 'f': force = true; break; + case 'h': human_readable = true; break; + case 'I': input_offset_str = optarg; break; + case 'i': input_path = optarg; break; + case 'O': output_offset_str = optarg; break; + case 'o': output_path = optarg; break; + case 'P': pad = true; break; + case 'p': progress_str = optarg; verbose = true; break; + case 'r': input_blksize_str = optarg; break; + case 's': sync = true; break; + case 't': truncate = true; break; + case 'v': verbose = true; break; + case 'w': output_blksize_str = optarg; break; + case 'x': no_create = true; break; + default: return 1; + } + } + + if ( optind < argc ) + errx(1, "unexpected extra operand"); + + if ( append && truncate ) + errx(1, "the -a and -t options are mutually incompatible"); + + int input_fd = 0; + if ( input_path ) + { + input_fd = open(input_path, O_RDONLY); + if ( input_fd < 0 ) + err(1, "%s", input_path); + } + else + input_path = ""; + + int output_fd = 1; + if ( output_path ) + { + int flags = O_WRONLY | O_CREAT; + if ( append ) + flags |= O_APPEND; + if ( no_create ) + flags |= O_EXCL; + output_fd = open(output_path, flags, 0666); + if ( output_fd < 0 ) + err(1, "%s", output_path); + } + else + { + if ( append ) + errx(1, "the -a option requires -o"); + output_path = ""; + } + + struct stat input_st; + if ( fstat(input_fd, &input_st) < 0 ) + err(1, "stat: %s", input_path); + +#if !defined(__sortix__) + if ( S_ISBLK(input_st.st_mode) && input_st.st_size == 0 ) + { + if ( (input_st.st_size = lseek(input_fd, 0, SEEK_END)) < 0 ) + err(1, "%s: lseek", input_path); + lseek(input_fd, 0, SEEK_SET); + } +#endif + + struct stat output_st; + if ( fstat(output_fd, &output_st) < 0 ) + err(1, "stat: %s", output_path); + +#if !defined(__sortix__) + if ( S_ISBLK(output_st.st_mode) && output_st.st_size == 0 ) + { + if ( (output_st.st_size = lseek(output_fd, 0, SEEK_END)) < 0 ) + err(1, "%s: lseek", output_path); + lseek(output_fd, 0, SEEK_SET); + } +#endif + + size_t input_blksize = input_st.st_blksize; + if ( input_blksize_str ) + input_blksize = parse_size_t(input_blksize_str, + input_st.st_blksize, + output_st.st_blksize); + if ( input_blksize == 0 ) + errx(1, "the input block size cannot be zero"); + + size_t output_blksize = output_st.st_blksize; + if ( output_blksize_str ) + output_blksize = parse_size_t(output_blksize_str, + input_st.st_blksize, + output_st.st_blksize); + if ( input_blksize == 0 ) + errx(1, "the output block size cannot be zero"); + + off_t input_offset = 0; + if ( input_offset_str ) + input_offset = parse_offset(input_offset_str, + input_blksize, + output_blksize, + input_st.st_size); + + off_t output_offset = 0; + if ( output_offset_str ) + output_offset = parse_offset(output_offset_str, + input_blksize, + output_blksize, + output_st.st_size); + if ( append ) + { + if ( output_offset != 0 ) + errx(1, "-O cannot be set to a non-zero value if -a is set"); + output_offset = output_st.st_size; + } + + off_t count = -1; // No limit. + if ( count_str ) + { + off_t left = input_offset <= input_st.st_size ? + input_st.st_size - input_offset : 0; + count = parse_offset(count_str, input_blksize, output_blksize, left); + } + + time_t interval = -1; // No interval. + if ( progress_str ) + interval = parse_time_t(progress_str); + + // Input and output are done only with aligned reads/writes, unless not + // possible. The buffer works in two modes depending on the parameters: + // + // 1) If + // + // * The input block size and output block sizes are a multiple of each + // other, and + // * the input offset and output offsets are equal modulo the block + // sizes; + // + // then the buffer size is the largest of the input block size and the + // output block size, and it will always be possible to fill the buffer + // of that size with input and write it out. + // + // 2) Otherwise, the buffer size is the input block size plus the output + // block size, working as a ring buffer. This buffer will ensure + // efficient forward progress can be made even with worst case block + // sizes and offsets. + bool use_largest_blksize = (input_blksize > output_blksize ? + input_blksize % output_blksize == 0 : + output_blksize % input_blksize == 0) && + input_offset % input_blksize == + output_offset % output_blksize; + size_t buffer_size = use_largest_blksize ? + input_blksize > output_blksize ? + input_blksize : + output_blksize : + input_blksize + output_blksize; + + // Allocate a page aligned buffer. + unsigned char* buffer = mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if ( buffer == MAP_FAILED ) + err(1, "allocating %zu byte buffer", buffer_size); + + struct timespec start; + clock_gettime(CLOCK_MONOTONIC, &start); + struct timespec last_statistic = start; + + if ( verbose ) + { + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = on_signal; + sa.sa_flags = SA_RESETHAND; // Second SIGINT is deadly. + sigaction(SIGINT, &sa, NULL); + } + + // Whether an end of file condition has been met, kept track of it in a + // variable to handle devices like terminals that don't have sticky EOF + // conditions (where the next read will also fail with an EOF condition). + bool input_eof = false; + + // Estimate of how much will be written to the output for statistics. This + // is set to -1 if not known or if the guess turns out to be wrong. + off_t estimated_total_out; + if ( S_ISREG(input_st.st_mode) || S_ISBLK(input_st.st_mode) ) + { + off_t remaining = input_offset <= input_st.st_size ? + input_st.st_size - input_offset : 0; + estimated_total_out = + count == -1 || remaining < count ? remaining : count; + } + else + estimated_total_out = count; + + // Skip past the initial input offset. If the input isn't seekable, read and + // discard that many bytes from the input. Fail hard even if -f as there is + // no way to recover. + if ( input_offset != 0 && lseek(input_fd, input_offset, SEEK_SET) < 0 ) + { + if ( errno != ESPIPE ) + err(1, "%s: lseek", input_path); + off_t offset = 0; + while ( !input_eof && offset < input_offset ) + { + size_t amount = input_blksize; + if ( (uintmax_t) (input_offset - offset) < (uintmax_t) amount ) + amount = input_offset - offset; + size_t so_far = 0; + while ( so_far < amount ) + { + progress(start, 0, estimated_total_out, human_readable, + &last_statistic, interval); + ssize_t done = read(input_fd, buffer + so_far, amount - so_far); + if ( done < 0 && errno == EINTR ) + done = 0; + else if ( done < 0 ) + err(1, "%s: offset %ji", input_path, (intmax_t) offset); + else if ( done == 0 ) + { + input_eof = true; + estimated_total_out = 0; + break; + } + so_far += done; + offset += done; + } + } + } + // The size of the next block to read, set such that after a block of this + // size has been read, all subsequent reads will be aligned. + size_t next_input_blksize = input_blksize - (input_offset % input_blksize); + + // Skip past the initial output offset. If the output isn't seekable, write + // that many NUL bytes to the output. Fail hard even if -f as there is no + // way to recover. If in append mode, -O is required to be zero and + // output_offset is already set to the size of the output. + if ( !append && + output_offset != 0 && + lseek(output_fd, output_offset, SEEK_SET) < 0 ) + { + if ( errno != ESPIPE ) + err(1, "%s: lseek", output_path); + memset(buffer, 0, output_blksize); + off_t offset = 0; + while ( offset < output_offset ) + { + size_t amount = output_blksize; + if ( (uintmax_t) (output_offset - offset) < (uintmax_t) amount ) + amount = output_offset - offset; + size_t so_far = 0; + while ( so_far < amount ) + { + progress(start, 0, estimated_total_out, human_readable, + &last_statistic, interval); + ssize_t done = + write(output_fd, buffer + so_far, amount - so_far); + if ( done < 0 && errno == EINTR ) + done = 0; + else if ( done < 0 ) + err(1, "%s: offset %ji", output_path, (intmax_t) offset); + so_far += done; + offset += done; + } + } + } + // The size of the next block to write, set such that after a block of this + // size has been written, all subsequent writes will be aligned. + size_t next_output_blksize = + output_blksize - (output_offset % output_blksize); + + // The total amount of bytes that has been read. + off_t total_in = 0; + + // The total amount of bytes that has been written. + off_t total_out = 0; + + // The offset in the ring buffer where data begins. + size_t buffer_offset = 0; + + // The amount of data bytes in the in the ring buffer. + size_t buffer_used = 0; + + // IO vector for efficient IO in case the ring buffer data wraps. + struct iovec iov[2]; + memset(iov, 0, sizeof(iov)); + + // The main loop. If an output block can't be written, read another input + // block. If an output block can be written, write it. + int exit_status = 0; + do + { + // Read another input block, unless enough data has already been read, + // or an end of file condition has been encountered. + if ( !input_eof && count != -1 && count <= total_in ) + { + input_eof = true; + estimated_total_out = total_in; + } + else if ( !input_eof && buffer_used < next_output_blksize ) + { + size_t left = next_input_blksize; + next_input_blksize = input_blksize; + if ( count != -1 && + (uintmax_t) (count - total_in) < (uintmax_t) left ) + left = count - total_in; + while ( left ) + { + progress(start, total_out, estimated_total_out, human_readable, + &last_statistic, interval); + assert(left <= buffer_size - buffer_used); + size_t buffer_end = buffer_offset + buffer_used; + if ( buffer_size < buffer_end ) + buffer_end -= buffer_size; + size_t sequential = buffer_size - buffer_end; + ssize_t done; + if ( left <= sequential ) + done = read(input_fd, buffer + buffer_end, left); + else + { + iov[0].iov_base = buffer + buffer_end; + iov[0].iov_len = sequential; + iov[1].iov_base = buffer; + iov[1].iov_len = left - sequential; + done = readv(input_fd, iov, 2); + } + if ( done < 0 && errno == EINTR ) + ; + else if ( done < 0 && !force ) + err(1, "%s: offset %ji", input_path, + (intmax_t) input_offset); + else if ( done == 0 ) + { + input_eof = true; + estimated_total_out = total_in; + break; + } + else + { + if ( done < 0 && force ) + { + warn("%s: offset %ji", input_path, + (intmax_t) input_offset); + // Skip until the next input block, or native input block + // (whichever comes first). + size_t until_next_native_block = + input_st.st_blksize - + (input_offset % input_st.st_blksize); + size_t skip = left < until_next_native_block ? + left : until_next_native_block; + // But don't skip past the end of the input. + off_t possible = input_offset <= input_st.st_size ? + input_st.st_size - input_offset : 0; + if ( (uintmax_t) possible < (uintmax_t) skip ) + skip = possible; + if ( lseek(input_fd, left, SEEK_CUR) < 0 ) + err(1, "%s: lseek", input_path); + // Check if we reached the end of the file. + if ( skip == 0 ) + { + input_eof = true; + estimated_total_out = total_in; + break; + } + if ( skip <= sequential ) + memset(buffer + buffer_end, 0, skip); + else + { + memset(buffer + buffer_end, 0, sequential); + memset(buffer, 0, skip - sequential); + } + done = skip; + exit_status = 1; + } + if ( OFF_MAX - input_offset < done ) + { + errno = EOVERFLOW; + err(1, "%s: offset", input_path); + } + left -= done; + input_offset += done; + buffer_used += done; + total_in += done; + // The estimate is wrong if too much has been read. + if ( estimated_total_out < total_in ) + estimated_total_out = -1; + } + } + } + // If requested, pad the final block with NUL bytes until the next + // output block size boundrary in the output. + if ( pad && (input_eof && 0 < buffer_used) && + buffer_used < next_output_blksize ) + { + size_t left = next_output_blksize - buffer_used; + size_t buffer_end = buffer_offset + buffer_used; + if ( buffer_size < buffer_end ) + buffer_end -= buffer_size; + size_t sequential = buffer_size - buffer_end; + if ( left <= sequential ) + memset(buffer + buffer_end, 0, left); + else + { + memset(buffer + buffer_end, 0, sequential); + memset(buffer, 0, left - sequential); + } + buffer_used = next_output_blksize; + estimated_total_out = total_out + buffer_used; + pad = false; + } + // If the end of the input has been reached or an fuæll output block can + // written out, write out an output block. + if ( (input_eof && 0 < buffer_used) || + next_output_blksize <= buffer_used ) + { + size_t left = next_output_blksize < buffer_used ? + next_output_blksize : buffer_used; + next_output_blksize = output_blksize; + while ( left ) + { + progress(start, total_out, estimated_total_out, human_readable, + &last_statistic, interval); + size_t sequential = buffer_size - buffer_offset; + ssize_t done; + if ( left <= sequential ) + done = write(output_fd, buffer + buffer_offset, left); + else + { + iov[0].iov_base = buffer + buffer_offset; + iov[0].iov_len = sequential; + iov[1].iov_base = buffer; + iov[1].iov_len = left - sequential; + done = writev(output_fd, iov, 2); + } + if ( done < 0 && errno == EINTR ) + ; + else if ( done < 0 && (!force || append) ) + err(1, "%s: offset %ji", output_path, + (intmax_t) output_offset); + else + { + // -f doesn't make sense in append mode as the error can't + // be skipped past. + if ( done < 0 && force && !append ) + { + warn("%s: offset %ji", output_path, + (intmax_t) output_offset); + // Skip until the next output block or native output + // block (whichever comes first). + size_t until_next_native_block = + output_st.st_blksize - + (output_offset % output_st.st_blksize); + size_t skip = left < until_next_native_block ? + left : until_next_native_block; + if ( lseek(output_fd, skip, SEEK_CUR) < 0 ) + err(1, "%s: lseek", output_path); + done = skip; + exit_status = 1; + } + if ( OFF_MAX - output_offset < done ) + { + errno = EOVERFLOW; + err(1, "%s: offset", output_path); + } + left -= done; + buffer_offset += done; + if ( buffer_size <= buffer_offset ) + buffer_offset -= buffer_size; + buffer_used -= done; + if ( buffer_used == 0 ) + buffer_offset = 0; + output_offset += done; + total_out += done; + // The estimate is wrong if too much has been written. + if ( estimated_total_out < total_out ) + estimated_total_out = -1; + } + } + } + } while ( !(input_eof && buffer_used == 0) ); + + munmap(buffer, buffer_size); + + if ( truncate && ftruncate(output_fd, output_offset) < 0 ) + err(1, "truncate: %s", output_path); + if ( sync && fsync(output_fd) < 0 ) + err(1, "sync: %s", output_path); + if ( close(input_fd) < 0 ) + err(1, "close: %s", input_path); + if ( close(output_fd) < 0 ) + err(1, "close: %s", output_path); + + if ( verbose || interrupted || signaled ) + { + signaled = 1; + progress(start, total_out, total_out, human_readable, &last_statistic, + interval); + } + + return exit_status; +} diff --git a/utils/command-not-found.c b/utils/command-not-found.c index b1803b69..c1b143e1 100644 --- a/utils/command-not-found.c +++ b/utils/command-not-found.c @@ -109,6 +109,12 @@ void suggest_reboot(const char* filename) } } +void suggest_rw(const char* filename) +{ + fprintf(stderr, "No command '%s' found, did you mean:\n", filename); + fprintf(stderr, " Command 'rw' from package 'rw'\n"); +} + int main(int argc, char* argv[]) { const char* filename = 2 <= argc ? argv[1] : argv[0]; @@ -134,6 +140,8 @@ int main(int argc, char* argv[]) suggest_poweroff(filename); else if ( !strcmp(filename, "reboot") ) suggest_reboot(filename); + else if ( !strcmp(filename, "dd") ) + suggest_rw(filename); fprintf(stderr, "%s: command not found\n", filename); return 127; }