commit: 7574df9ac786b3be7115588d0d73f4e44776fea8
parent a33474573598a9985c20e47da5a81e8e57c19780
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Fri, 28 Jul 2023 12:55:02 +0200
cmd/base64: New command
Diffstat:
3 files changed, 252 insertions(+), 0 deletions(-)
diff --git a/cmd/base64.c b/cmd/base64.c
@@ -0,0 +1,171 @@
+// Collection of Unix tools, comparable to coreutils
+// SPDX-FileCopyrightText: 2017-2023 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
+
+#define _POSIX_C_SOURCE 200809L
+#include <assert.h> /* assert */
+#include <errno.h>  /* errno */
+#include <fcntl.h>  /* open(), O_RDONLY */
+#include <stdint.h> /* uint8_t */
+#include <stdio.h>  /* fprintf(), BUFSIZ */
+#include <string.h> /* strerror(), strncmp() */
+#include <unistd.h> /* read(), write(), close(), getopt(), opt* */
+
+// 64(26+26+10+2)
+static char b64_encmap[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+static size_t c_out        = 0;
+// TODO: -w option ; 76 is lowest of all base64 related RFCs
+static size_t wrap_nl = 76;
+
+static inline uint8_t
+b64_get(uint8_t pos)
+{
+	assert(pos <= 64);
+	return b64_encmap[pos];
+}
+
+static void
+b64encode(uint8_t out[4], uint8_t in[3])
+{
+	out[0] = b64_get(in[0] >> 2);
+	out[1] = b64_get((uint8_t)(((in[0] & 0x03) << 4) | (in[1] >> 4)));
+	out[2] = b64_get((uint8_t)(((in[1] & 0x0f) << 2) | (in[2] >> 6)));
+	out[3] = b64_get(in[2] & 0x3f);
+}
+
+static int
+encode(int fd, const char *fdname)
+{
+	ssize_t c = 0;
+	char ibuf[3];
+
+	while((c = read(fd, ibuf, sizeof(ibuf))) > 0)
+	{
+		uint8_t obuf[4] = "----";
+		ssize_t pad  = 0;
+		assert(pad >= 0);
+
+		for(; c < 3; pad++, c++)
+		{
+			ibuf[c] = 0;
+		}
+		assert(c == 3);
+
+		b64encode(obuf, (uint8_t *)ibuf);
+
+		for(; pad > 0; pad--)
+		{
+			obuf[4 - pad] = '=';
+		}
+
+		if(write(1, (char *)obuf, 4) != 4)
+		{
+			fprintf(stderr, "base64: Error writing: %s\n", strerror(errno));
+			return 1;
+		}
+		c_out += 4;
+
+		if(wrap_nl != 0 && c_out >= wrap_nl)
+		{
+			c_out = 0;
+			if(write(1, "\n", 1) != 1)
+			{
+				fprintf(stderr, "base64: Error writing: %s\n", strerror(errno));
+				return 1;
+			}
+		}
+	}
+
+	if(c < 0)
+	{
+		fprintf(stderr, "base64: Error reading ‘%s’: %s\n", fdname, strerror(errno));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+decode(int fd, const char *fdname)
+{
+	return 1;
+}
+
+int
+main(int argc, char *argv[])
+{
+	int (*process)(int, const char *) = &encode;
+
+	int c = 0, ret = 0;
+
+	while((c = getopt(argc, argv, ":d")) != -1)
+	{
+		switch(c)
+		{
+		case 'd':
+			process = &decode;
+			break;
+		case ':':
+			fprintf(stderr, "base64: Error: Missing operand for option: ‘-%c’\n", optopt);
+			return 1;
+		case '?':
+			fprintf(stderr, "base64: Error: Unrecognised option: ‘-%c’\n", optopt);
+			return 1;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if(argc <= 0)
+	{
+		ret = process(0, "<stdin>");
+		goto end;
+	}
+
+	for(int argi = 0; argi < argc; argi++)
+	{
+		if(strncmp(argv[argi], "-", 2) == 0)
+		{
+			if(process(0, "<stdin>") != 0)
+			{
+				ret = 1;
+				goto end;
+			}
+		}
+		else if(strncmp(argv[argi], "--", 3) == 0)
+		{
+			continue;
+		}
+		else
+		{
+			int fd = open(argv[argi], O_RDONLY);
+			if(fd < 0)
+			{
+				fprintf(stderr, "base64: Error opening ‘%s’: %s\n", argv[argi], strerror(errno));
+				ret = 1;
+				goto end;
+			}
+
+			if(process(fd, argv[argi]) != 0)
+			{
+				ret = 1;
+				goto end;
+			}
+
+			if(close(fd) < 0)
+			{
+				fprintf(stderr, "base64: Error closing ‘%s’: %s\n", argv[argi], strerror(errno));
+				ret = 1;
+				goto end;
+			}
+		}
+	}
+
+end:
+	if(c_out > 0)
+	{
+		printf("\n");
+	}
+	return ret;
+}
diff --git a/test-cmd/Kyuafile b/test-cmd/Kyuafile
@@ -10,6 +10,7 @@ basedir = fs.dirname(fs.dirname(current_kyuafile()))
 -- atf_test_program{name="pat", required_files=basedir.."/cmd/pat", timeout=1}
 atf_test_program{name="args", required_files=basedir.."/cmd/args", timeout=1}
 atf_test_program{name="basename", required_files=basedir.."/cmd/basename", timeout=1}
+atf_test_program{name="base64", required_files=basedir.."/cmd/base64", timeout=1}
 atf_test_program{name="cat", required_files=basedir.."/cmd/cat", timeout=1}
 atf_test_program{name="date", required_files=basedir.."/cmd/date", timeout=1}
 atf_test_program{name="del", required_files=basedir.."/cmd/del", timeout=1}
diff --git a/test-cmd/base64 b/test-cmd/base64
@@ -0,0 +1,80 @@
+#!/usr/bin/env atf-sh
+# SPDX-FileCopyrightText: 2017-2022 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
+
+atf_test_case allbytes
+allbytes_body() {
+	atf_check -o file:outputs/base64/all_bytes ../cmd/base64 inputs/all_bytes
+	atf_check -o file:outputs/base64/all_bytes ../cmd/base64 <inputs/all_bytes
+	atf_check -o file:outputs/base64/all_bytes ../cmd/base64 - <inputs/all_bytes
+}
+
+atf_test_case devnull
+devnull_body() {
+	atf_check ../cmd/base64 /dev/null
+	atf_check ../cmd/base64 </dev/null
+	atf_check ../cmd/base64 - </dev/null
+}
+
+atf_test_case rfc4648
+rfc4648_body() {
+	# Test vectors from RFC4648
+	atf_check -o 'inline:'         sh -c 'printf "" | ../cmd/base64'
+	atf_check -o 'inline:Zg==\n'     sh -c 'printf "f" | ../cmd/base64'
+	atf_check -o 'inline:Zm8=\n'     sh -c 'printf "fo" | ../cmd/base64'
+	atf_check -o 'inline:Zm9v\n'     sh -c 'printf "foo" | ../cmd/base64'
+	atf_check -o 'inline:Zm9vYg==\n' sh -c 'printf "foob" | ../cmd/base64'
+	atf_check -o 'inline:Zm9vYmE=\n' sh -c 'printf "fooba" | ../cmd/base64'
+	atf_check -o 'inline:Zm9vYmFy\n' sh -c 'printf "foobar" | ../cmd/base64'
+}
+
+atf_test_case noperm cleanup
+noperm_body() {
+	touch inputs/chmod_000 || atf_fail "touching chmod_000"
+	chmod 0000 inputs/chmod_000 || atf_fail "chmod 0000 chmod_000"
+	# shellcheck disable=SC1112
+	atf_check -s exit:1 -e 'inline:base64: Error opening ‘inputs/chmod_000’: Permission denied\n' ../cmd/base64 inputs/chmod_000
+}
+noperm_cleanup() {
+	chmod 0600 inputs/chmod_000 || atf_fail "chmod 0600 chmod_000"
+	rm inputs/chmod_000 || atf_fail "rm chmod_000"
+}
+
+atf_test_case devfull
+devfull_body() {
+	atf_check -s exit:1 -e 'inline:base64: Error writing: No space left on device\n' sh -c '../cmd/base64 inputs/all_bytes >/dev/full'
+	atf_check -s exit:1 -e 'inline:base64: Error writing: No space left on device\n' sh -c '../cmd/base64 <inputs/all_bytes >/dev/full'
+	atf_check -s exit:1 -e 'inline:base64: Error writing: No space left on device\n' sh -c '../cmd/base64 - <inputs/all_bytes >/dev/full'
+}
+
+atf_test_case readslash
+readslash_body() {
+	[ "$(uname -s)" = "NetBSD" ] && atf_skip "NetBSD allows to read directories"
+
+	atf_check -s exit:1 -e "inline:base64: Error reading ‘/’: Is a directory\n" ../cmd/base64 /
+}
+
+atf_test_case enoent
+enoent_body() {
+	# shellcheck disable=SC1112
+	atf_check -s exit:1 -e 'inline:base64: Error opening ‘/var/empty/e/no/ent’: No such file or directory\n' ../cmd/base64 /var/empty/e/no/ent
+}
+
+atf_test_case doubledash
+doubledash_body() {
+	atf_check -o file:outputs/base64/all_bytes -- ../cmd/base64 -- inputs/all_bytes
+	# shellcheck disable=SC1112
+	atf_check -s exit:1 -e "inline:base64: Error: Unrecognised option: ‘--’\n" -o empty -- ../cmd/base64 --- inputs/all_bytes
+}
+
+atf_init_test_cases() {
+	cd "$(atf_get_srcdir)" || exit 1
+	atf_add_test_case rfc4648
+	atf_add_test_case allbytes
+	atf_add_test_case devnull
+	atf_add_test_case noperm
+	atf_add_test_case devfull
+	atf_add_test_case readslash
+	atf_add_test_case enoent
+	atf_add_test_case doubledash
+}