Add cat
authorAlessio Chiapperini <[email protected]>
Wed, 6 Apr 2022 12:52:53 +0000 (6 14:52 +0200)
committerAlessio Chiapperini <[email protected]>
Wed, 6 Apr 2022 12:52:53 +0000 (6 14:52 +0200)
.gitignore
src/cat/Makefile [new file with mode: 0644]
src/cat/cat.1 [new file with mode: 0644]
src/cat/cat.c [new file with mode: 0644]
src/cat/config.mk [new file with mode: 0644]
test/cat.sh [new file with mode: 0644]
test/harness.sh
test/rm.sh

index 60db41f..c40060f 100644 (file)
@@ -5,3 +5,4 @@ src/**/*.o
 src/false/false
 src/true/true
 src/rm/rm
+src/cat/cat
diff --git a/src/cat/Makefile b/src/cat/Makefile
new file mode 100644 (file)
index 0000000..c184601
--- /dev/null
@@ -0,0 +1,28 @@
+.POSIX:
+
+.PHONY: all clean install uninstall
+
+include config.mk
+
+SRCS != echo *.c
+OBJS = ${SRCS:.c=.o}
+
+all: ${PROG}
+
+${PROG}: ${OBJS}
+       ${CC} ${LDFLAGS} -o $@ ${OBJS} ${LDLIBS}
+
+${PROG}.o: ${PROG}.c
+
+clean:
+       ${RM} -f ${OBJS} ${PROG}
+
+install: ${PROG} ${PROG}.1
+       mkdir -p ${DESTDIR}${PREFIX}/bin
+       install -m 755 ${PROG} ${DESTDIR}${PREFIX}/bin
+       mkdir -p ${DESTDIR}${MANPREFIX}/man1
+       gzip < ${PROG}.1 > ${DESTDIR}${MANPREFIX}/man1/${PROG}.1.gz
+
+uninstall:
+       ${RM} -f ${DESTDIR}${PREFIX}/bin/${PROG}
+       ${RM} -f ${DESTDIR}${MANPREFIX}/man1/${PROG}.1.gz
diff --git a/src/cat/cat.1 b/src/cat/cat.1
new file mode 100644 (file)
index 0000000..442c02b
--- /dev/null
@@ -0,0 +1,75 @@
+.\" SPDX-License-Identifier: BSD-2-Clause
+
+.\" Copyright (c) 2022 Alessio Chiapperini
+
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in the
+.\"    documentation and/or other materials provided with the distribution.
+
+.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+
+.Dd April 6, 2022
+.Dt CAT 1
+.Os
+.Sh NAME
+.Nm cat
+.Nd concatenate and print files in reverse.
+.Sh SYNOPSIS
+.Nm
+.Op Fl \&hu
+.Op Ar file ...
+.Sh DESCRIPTION
+The
+.Nm
+utility reads files sequentially, writing them to standard output in reverse
+starting from the last character. The
+.Ar file
+operands are processed in command-line order. If
+.Ar file
+is a single dash
+.Pq Sq Fl
+or absent,
+.Nm
+reads from standard input.
+.Pp
+The options are as follows:
+.Bl -tag -width indent
+.It Fl h
+Print usage information and exit.
+.It Fl u
+Write bytes from the input file to the standard output without delay as each is read.
+.El
+.Sh "EXIT STATUS"
+The
+.Nm
+utility exits 0 on success, and 1 if an error occurs.
+.Sh EXAMPLES
+Backup the file named
+.Ar foo
+and ask for confirmation:
+.Dl $ rm -i foo
+.Pp
+Any of these commands will backup the file
+.Ar -f :
+.Dl $ rm -- -f
+.Dl $ rm ./-f
+.Sh SEE ALSO
+.Xr rev 1
+.Sh AUTHOR
+.An Alessio Chiapperini Aq Mt alessio.chiapperini@\:nullbuffer.com 
+
diff --git a/src/cat/cat.c b/src/cat/cat.c
new file mode 100644 (file)
index 0000000..6082437
--- /dev/null
@@ -0,0 +1,243 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2022 Alessio Chiapperini
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _POSIX_C_SOURCE
+#  define _POSIX_C_SOURCE 200112L
+#elif _POSIX_C_SOURCE < 200112L
+#  error incompatible _POSIX_C_SOURCE level
+#endif
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define N 32768
+#define PREFETCH (32*N)
+
+static void
+usage(void)
+{
+       (void)fprintf(stderr, "usage: cat [-u] [file...]\n");
+       exit(1);
+}
+
+static void
+memrev(char *dst, const char *src, size_t len)
+{
+       for (size_t i = 0; i < len; i++) {
+               dst[i] = src[len-1-i];
+       }
+}
+
+static int
+print_rev(char *buf, off_t off)
+{
+       char dst[N];
+       int ret;
+       size_t blen = off % N ? off % N : N;
+
+       ret = 0;
+       while (off > 0) {
+               off -= blen;
+
+               if (off > PREFETCH) {
+                       (void)posix_madvise(buf + off - PREFETCH, PREFETCH,
+                           POSIX_MADV_WILLNEED);
+               }
+               memrev(dst, buf + off, blen);
+               if (write(STDOUT_FILENO, dst, blen) < (ssize_t)blen) {
+                       ret = 1;
+                       perror("cat");
+                       goto err;
+               }
+
+               blen = N;
+       }
+
+err:
+       return (ret);
+}
+
+static int
+cat_buffered(const int fd)
+{
+       FILE *ifile;
+       ssize_t buflen, bufsize, nbufsize;
+       char *buf, *nbuf;
+       int ch;
+       int ret;
+
+       ifile = fdopen(fd, "r");
+       if (ifile == 0) {
+               ret = 1;
+               perror("cat");
+               goto out;
+       }
+
+       buflen = 0;
+       bufsize = BUFSIZ;
+       buf = calloc((size_t)bufsize, 1);
+       if (buf == 0) {
+               ret = 1;
+               perror("cat");
+               goto calloc_err;
+       }
+
+       while ((ch = fgetc(ifile)) != EOF) {
+               buf[buflen++] = (char)ch;
+
+               if (buflen == bufsize) {
+                       nbufsize = bufsize << 1;
+                       if ((nbuf = realloc(buf, (size_t)nbufsize)) == 0) {
+                               ret = 1;
+                               perror("cat");
+                               goto realloc_err;
+                       }
+                       buf = nbuf;
+                       bufsize = nbufsize;
+               }
+       }
+       buf[buflen] = 0;
+
+       ret = print_rev(buf, buflen);
+
+realloc_err:
+       free(buf);
+       buf = 0;
+calloc_err:
+       (void)fclose(ifile);
+out:
+       return (ret);
+}
+
+static int
+cat_mmap(const int fd)
+{
+       struct stat s;
+       int ret;
+       char *f;
+
+       if (fstat(fd, &s) < 0) {
+               ret = 1;
+               perror("cat");
+               goto err;
+       }
+
+       f = mmap(0, (size_t)s.st_size, PROT_READ, MAP_SHARED, fd, 0);
+       if (f == MAP_FAILED) {
+               ret = 1;
+               perror("cat");
+               goto err;
+       }
+
+       ret = print_rev(f, s.st_size);
+
+       if (munmap(f, (size_t)s.st_size) < 0) {
+               ret = 1;
+               perror("cat");
+               goto err;
+       }
+
+err:
+       return (ret);
+}
+
+static int
+cat(const char *src)
+{
+       int fd;
+       int ret;
+
+       fd = STDIN_FILENO;
+       if (src[0] != '-' || src[1] != '\0') {
+               fd = open(src, O_RDONLY);
+               if (fd < 0) {
+                       ret = 1;
+                       perror("cat");
+                       goto open_err;
+               }
+       }
+
+       if (fd == STDIN_FILENO || lseek(STDIN_FILENO, 0, SEEK_CUR) == -1) {
+               ret = cat_buffered(fd);
+
+       } else {
+               ret = cat_mmap(fd);
+       }
+
+       close(fd);
+open_err:
+       return (ret);
+}
+
+int
+main(int argc, char *argv[])
+{
+
+       char *def[] = { "-" };
+       int opt;
+
+       while ((opt = getopt(argc, argv, "u")) != -1) {
+               switch (opt) {
+               case 'u':
+                       /*
+                        * "Write bytes from the input file to the standard
+                        * output without delay as each is read", which we were
+                        * going to do anyway.
+                        */
+                       break;
+               case '?':
+                       /* FALLTHROUGH */
+               default:
+                       usage();
+               }
+       }
+
+       argc -= optind;
+       argv += optind;
+
+       if (argc == 0) {
+               argc = 1;
+               argv = def;
+       }
+
+       for (int i = 0; i < argc; i++) {
+               if (cat(argv[i]) > 0) {
+                       return (1);
+               }
+       }
+
+       return (0);
+}
+
diff --git a/src/cat/config.mk b/src/cat/config.mk
new file mode 100644 (file)
index 0000000..fd1b842
--- /dev/null
@@ -0,0 +1,26 @@
+PROG = cat
+
+# compiler
+CC = cc
+
+# flags
+CFLAGS = -std=c99 -O2 -Wall -Wextra -Wpedantic -Werror \
+       -Walloca -Wcast-qual -Wconversion -Wformat=2 -Wformat-security \
+       -Wnull-dereference -Wstack-protector -Wvla -Warray-bounds \
+       -Wbad-function-cast -Wconversion -Wshadow -Wstrict-overflow=4 -Wundef \
+       -Wstrict-prototypes -Wswitch-default -Wfloat-equal -Wimplicit-fallthrough \
+       -Wpointer-arith -Wswitch-enum \
+       -D_FORTIFY_SOURCE=2 \
+       -fstack-protector-strong -fPIE -fstack-clash-protection
+
+LDFLAGS = -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack -Wl,-z,separate-code
+
+# libs
+LDLIBS =
+
+# paths
+PREFIX = /usr/local
+MANPREFIX = ${PREFIX}/share/man
+
+RM = rm
+
diff --git a/test/cat.sh b/test/cat.sh
new file mode 100644 (file)
index 0000000..1ba8313
--- /dev/null
@@ -0,0 +1,44 @@
+echo -n "this is a test file" > "$TMPDIR"/test-file-1
+echo -n "this is another test file" > "$TMPDIR"/test-file-2
+echo -n "dashed" > ${TMPDIR}/-dashed
+
+cat_should_handle_one_file() {
+    tool=`find ../src -type f -name "cat"`
+    ct="$(${tool} ${TMPDIR}/test-file-1)"
+    ref=$(rev ${TMPDIR}/test-file-1)
+
+    [ "$ct" = "$ref" ]
+}
+
+cat_should_handle_two_files() {
+    tool=`find ../src -type f -name "cat"`
+    ct="$(${tool} ${TMPDIR}/test-file-1 ${TMPDIR}/test-file-2)"
+    ref1=$(rev ${TMPDIR}/test-file-1)
+    ref2=$(rev ${TMPDIR}/test-file-2)
+    ref=$(echo -n "$ref1$ref2")
+
+    [ "$ct" = "$ref" ]
+}
+
+cat_should_handle_stdin() {
+    tool=`find ../src -type f -name "cat"`
+    ct="$(echo -n "this is from stdin" | ${tool} -)"
+    ref=$(echo -n "this is from stdin" | rev)
+    [ "$ct" = "$ref" ] || return 1
+    ct="$(echo -n "this is from stdin" | ${tool})"
+    [ "$ct" = "$ref" ]
+}
+
+cat_should_handle_u_flag() {
+    # The default behavior is to flush at every write
+    ct="$(echo -n "this is from stdin" | ${tool} -u)"
+    ref=$(echo -n "this is from stdin" | rev)
+    [ "$ct" = "$ref" ]
+}
+
+cat_should_handle_dash() {
+    tool=`find ../src -type f -name "cat"`
+    ct="$(${tool} -- ${TMPDIR}/-dashed)"
+    ref=$(echo -n "dashed" | rev)
+    [ "$ct" = "$ref" ]
+}
index 1808d11..5df2b8b 100755 (executable)
@@ -7,10 +7,10 @@ mkdir -p "$TMPDIR"
 
 setup() {
     # Dashed file
-    touch -- ${TMPDIR}/-foo
+    touch -- ${TMPDIR}/-dashed
 
     # Simple file
-    echo -e "foo\nbar" > ${TMPDIR}/bar
+    echo -e "foo\nbar" > ${TMPDIR}/simple
 
     # Symbolic link
     echo "orig" > ${TMPDIR}/orig
@@ -59,6 +59,7 @@ run_tests() {
 . $(dirname "$0")/false.sh
 . $(dirname "$0")/true.sh
 . $(dirname "$0")/rm.sh
+. $(dirname "$0")/cat.sh
 
 setup
 run_tests \
@@ -67,4 +68,9 @@ run_tests \
     rm_backup_file \
     rm_should_handle_dash \
     rm_should_handle_directory \
-    rm_should_follow_symlinks
+    rm_should_follow_symlinks \
+    cat_should_handle_one_file \
+    cat_should_handle_two_files \
+    cat_should_handle_stdin \
+    cat_should_handle_u_flag \
+    cat_should_handle_dash
index f249fed..eaa2127 100644 (file)
@@ -1,18 +1,18 @@
 rm_backup_file() {
     tool=`find ../src -type f -name "rm"`
-    ${tool} ${TMPDIR}/bar
-    diff ${TMPDIR}/bar ${TMPDIR}/bar.bak
+    ${tool} ${TMPDIR}/simple
+    diff ${TMPDIR}/simple ${TMPDIR}/simple.bak
 }
 
 rm_should_handle_dash() {
     tool=`find ../src -type f -name "rm"`
-    ${tool} -- ${TMPDIR}/-foo
-    [ -e ${TMPDIR}/-foo -a -e ${TMPDIR}/-foo.bak ]
+    ${tool} -- ${TMPDIR}/-dashed
+    [ -e ${TMPDIR}/-dashed -a -e ${TMPDIR}/-dashed.bak ]
 }
 
 rm_should_handle_directory() {
     tool=`find ../src -type f -name "rm"`
-    ${tool} ${TMPDIR}/
+    ${tool} ${TMPDIR}/ 2> /dev/null
     [ $? = 0 -a ! -f ${TMPDIR}/.bak ]
 }