commit: b1ce552adedbaa4b74341a1635fe12111aabbee9
parent 9d95b95eb003df990d5fabbc19ac8601c674f925
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Tue, 25 Mar 2025 05:25:52 +0100
cmd/uuencode: new
Diffstat:
8 files changed, 334 insertions(+), 17 deletions(-)
diff --git a/cmd/base64.1 b/cmd/base64.1
@@ -51,6 +51,8 @@ is 0, then no newlines are written.
 .El
 .Sh EXIT STATUS
 .Ex -std
+.Sh SEE ALSO
+.Xr uuencode 1
 .Sh STANDARDS
 .Nm
 follows base64 as defined in RFC3548 and RFC4648.
diff --git a/cmd/base64.c b/cmd/base64.c
@@ -6,14 +6,15 @@
 #define _POSIX_C_SOURCE 200809L
 #include "../lib/getopt_nolong.h"
 
-#include <assert.h> /* assert */
-#include <ctype.h>  /* isspace */
-#include <errno.h>  /* errno */
-#include <stdint.h> /* uint8_t */
-#include <stdio.h>  /* fopen(), fprintf(), BUFSIZ */
-#include <stdlib.h> /* abort */
-#include <string.h> /* strerror(), strncmp() */
-#include <unistd.h> /* read(), write(), close() */
+#include <assert.h>   /* assert */
+#include <ctype.h>    /* isspace */
+#include <errno.h>    /* errno */
+#include <stdint.h>   /* uint8_t */
+#include <stdio.h>    /* fopen(), fprintf() */
+#include <stdlib.h>   /* abort */
+#include <string.h>   /* strerror(), strncmp() */
+#include <sys/stat.h> /* fstat */
+#include <unistd.h>   /* read(), write(), close(), getopt() */
 
 // 64(26+26+10+2) + NULL
 static const char *b64_encmap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -60,7 +61,7 @@ b64encode(uint8_t out[4], uint8_t in[3])
 }
 
 static int
-encode(FILE *fin, const char *name)
+base64_encode(FILE *fin, const char *iname)
 {
 	while(1)
 	{
@@ -85,7 +86,7 @@ encode(FILE *fin, const char *name)
 		if(ferror(fin))
 		{
 			fprintf(
-			    stderr, "%s: error: Failed reading from file '%s': %s\n", argv0, name, strerror(errno));
+			    stderr, "%s: error: Failed reading from file '%s': %s\n", argv0, iname, strerror(errno));
 			errno = 0;
 			return 1;
 		}
@@ -145,7 +146,7 @@ encode(FILE *fin, const char *name)
 // This code is derived from software contributed to The NetBSD Foundation
 // by Christos Zoulas.
 static int
-decode(FILE *fin, const char *name)
+base64_decode(FILE *fin, const char *iname)
 {
 	int c = 0;
 	uint8_t out = 0;
@@ -254,10 +255,11 @@ decode(FILE *fin, const char *name)
 	return 0;
 }
 
-int
-main(int argc, char *argv[])
+static int
+base64_main(int argc, char *argv[])
 {
-	int (*process)(FILE *, const char *) = &encode;
+	argv0 = "base64";
+	int (*process)(FILE *, const char *) = &base64_encode;
 
 	int ret = 0;
 
@@ -266,7 +268,7 @@ main(int argc, char *argv[])
 		switch(c)
 		{
 		case 'd':
-			process = &decode;
+			process = &base64_decode;
 			break;
 		case 'w':
 			errno = 0;
@@ -363,3 +365,221 @@ end:
 
 	return ret;
 }
+
+static int
+uuencode(FILE *fin, const char *iname)
+{
+	while(1)
+	{
+		char output[60] = "";
+
+		size_t pos = 0;
+		int len = 0;
+		for(; len < 45; pos += 4, len += 3)
+		{
+			uint8_t ibuf[3] = {0, 0, 0};
+
+			size_t c = 0;
+			for(; c < 3; c++)
+			{
+				int buf = getc(fin);
+				if(buf == EOF)
+				{
+					break;
+				}
+				ibuf[c] = buf;
+			}
+			if(c == 0)
+			{
+				break;
+			}
+			if(ferror(fin))
+			{
+				fprintf(stderr,
+				        "%s: error: Failed reading from file '%s': %s\n",
+				        argv0,
+				        iname,
+				        strerror(errno));
+				errno = 0;
+				return 1;
+			}
+
+			assert((3 + pos) < 60);
+			/* conversion math taken from POSIX.1-2024 specification */
+			/* clang-format off */
+			output[0+pos] = 0x20 + (( ibuf[0] >> 2                           ) & 0x3F);
+			output[1+pos] = 0x20 + (((ibuf[0] << 4) | ((ibuf[1] >> 4) & 0x0F)) & 0x3F);
+			output[2+pos] = 0x20 + (((ibuf[1] << 2) | ((ibuf[2] >> 6) & 0x03)) & 0x3F);
+			output[3+pos] = 0x20 + (( ibuf[2]                                ) & 0x3F);
+			/* clang-format on */
+
+			for(int i = 0; i < 4; i++)
+				if(output[i + pos] == 0x20) output[i + pos] = 0x60;
+		}
+
+		if(printf("%c", len == 0 ? 0x60 : 0x20 + len) < 0)
+		{
+			fprintf(stderr, "%s: error: Failed writing length: %s\n", argv0, strerror(errno));
+			errno = 0;
+			return 1;
+		}
+
+		if(pos > 0)
+			if(fwrite(output, pos, 1, stdout) <= 0)
+			{
+				fprintf(stderr, "%s: error: Failed writing data: %s\n", argv0, strerror(errno));
+				errno = 0;
+				return 1;
+			}
+
+		if(printf("\n") < 0)
+		{
+			fprintf(stderr, "%s: error: Failed writing newline: %s\n", argv0, strerror(errno));
+			errno = 0;
+			return 1;
+		}
+
+		if(feof(fin)) break;
+	}
+
+	if(fflush(stdout) != 0)
+	{
+		fprintf(stderr, "%s: error: Failed writing (flush): %s\n", argv0, strerror(errno));
+		errno = 0;
+		return 1;
+	}
+	int err = ferror(stdout);
+	if(err != 0)
+	{
+		fprintf(stderr, "%s: error: Failed writing (ferror): %s\n", argv0, strerror(errno));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+uuencode_main(int argc, char *argv[])
+{
+	argv0 = "uuencode";
+	const char *begin_str = "begin";
+	const char *end_str = "end\n";
+	int (*process)(FILE *, const char *) = &uuencode;
+
+	// get old, then reset to old. Yay to POSIX nonsense
+	mode_t mask = umask(0);
+	umask(mask);
+
+	// negate to get a normal bitmask
+	mask ^= 0777;
+
+	wrap_nl = 45;
+
+	int ret = 0;
+
+	for(int c = -1; (c = getopt_nolong(argc, argv, ":m")) != -1;)
+	{
+		switch(c)
+		{
+		case 'm':
+			process = &base64_encode;
+			begin_str = "begin-base64";
+			end_str = "====\n";
+			wrap_nl = 76;
+			break;
+		case ':':
+			fprintf(stderr, "%s: error: Missing operand for option '-%c'\n", argv0, optopt);
+			return 1;
+		case '?':
+			if(!got_long_opt) fprintf(stderr, "%s: error: Unrecognised option '-%c'\n", argv0, optopt);
+			return 1;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if(argc > 2)
+	{
+		fprintf(stderr, "%s: error: Expected 2 arguments or less, got %d\n", argv0, argc);
+		return 1;
+	}
+
+	FILE *fin = stdin;
+	const char *decode_path = "-";
+	const char *iname = "<stdin>";
+	mode_t mode = 0666 & mask;
+
+	if(argc >= 1) decode_path = argv[argc - 1];
+
+	if(argc == 2)
+	{
+		iname = argv[0];
+
+		fin = fopen(iname, "rb");
+		if(!fin)
+		{
+			fprintf(
+			    stderr, "%s: error: Failed opening input file '%s': %s\n", argv0, iname, strerror(errno));
+			return 1;
+		}
+
+		struct stat fin_stat;
+		if(fstat(fileno(fin), &fin_stat) != 0)
+		{
+			fprintf(stderr,
+			        "%s: warning: Failed getting status for file '%s': %s\n",
+			        argv0,
+			        iname,
+			        strerror(errno));
+			ret = 1;
+		}
+
+		mode = fin_stat.st_mode & 0777;
+	}
+
+	printf("%s %03o %s\n", begin_str, mode, decode_path);
+	if(process(fin, iname) != 0)
+	{
+		ret = 1;
+		goto end;
+	}
+
+end:
+	if(ret == 0)
+	{
+		if(wrap_nl != 0 && c_out > 0) printf("\n");
+
+		printf("%s", end_str);
+	}
+
+	if(fclose(fin) != 0)
+	{
+		fprintf(stderr, "%s: error: Failed closing file \"%s\": %s\n", argv0, iname, strerror(errno));
+		ret = 1;
+	}
+
+	if(fclose(stdout) != 0)
+	{
+		fprintf(stderr, "%s: error: Failed closing file <stdout>: %s\n", argv0, strerror(errno));
+		ret = 1;
+	}
+
+	return ret;
+}
+
+int
+main(int argc, char *argv[])
+{
+	const char *arg0 = argv[0];
+
+	const char *s = strrchr(arg0, '/') + 1;
+	if(s) arg0 = s;
+
+	if(strcmp(arg0, "base64") == 0) return base64_main(argc, argv);
+	if(strcmp(arg0, "uuencode") == 0) return uuencode_main(argc, argv);
+
+	fprintf(stderr, "%s: error: Unknown utility '%s' expected 'base64' or 'uuencode'\n", arg0, arg0);
+
+	return -1;
+}
diff --git a/cmd/uuencode b/cmd/uuencode
@@ -0,0 +1 @@
+base64
+\ No newline at end of file
diff --git a/cmd/uuencode.1 b/cmd/uuencode.1
@@ -0,0 +1,44 @@
+.\" utils-std: Collection of commonly available Unix tools
+.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd March 25, 2025
+.Dt UUENCODE 1
+.Os
+.Sh NAME
+.Nm uuencode
+.Nd encode data into uuencoding/base64 to standard output
+.Sh SYNOPSIS
+.Nm
+.Op Fl m
+.Op Oo Ar file Oc Ar decode_pathname
+.Sh DESCRIPTION
+.Nm
+reads
+.Ar file ,
+encode it in uuencoding or base64 and writes the results into standard output.
+If no
+.Ar file
+is given,
+.Nm
+reads from the standard input.
+If no
+.Ar decode_pathname
+is given for the path printed in the output, then
+.Ql -
+is used.
+.Sh OPTIONS
+.Bl -tag -width _m
+.It Fl m
+Encode data into base64
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr base64 1
+.Sh STANDARDS
+.Nm
+should be compliant with the
+IEEE Std 1003.1-2024 (“POSIX.1”)
+specification.
+.Sh AUTHORS
+.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/posix_utilities.txt b/posix_utilities.txt
@@ -143,8 +143,8 @@ unget: no, SCCS XOPEN_UNIX
 uniq: done
 unlink: done
 uucp: no, XOPEN_UUCP
-uudecode: no, external/obsolete
-uuencode: no, external/obsolete
+uudecode
+uuencode: done
 uustat: no, XOPEN_UUCP
 uux: no, XOPEN_UUCP
 val: no, SCCS XOPEN_UNIX
diff --git a/test-cmd/outputs/uuencode/all_bytes b/test-cmd/outputs/uuencode/all_bytes
@@ -0,0 +1,8 @@
+begin 644 all_bytes
+M``$"`P0%!@<("0H+#`T.#Q`1$A,4%187&!D:&QP='A\@(2(C)"4F)R@I*BLL
+M+2XO,#$R,S0U-C<X.3H[/#T^/T!!0D-$149'2$E*2TQ-3D]045)35%565UA9
+M6EM<75Y?8&%B8V1E9F=H:6IK;&UN;W!Q<G-T=79W>'EZ>WQ]?G^`@8*#A(6&
+MAXB)BHN,C8Z/D)&2DY25EI>8F9J;G)V>GZ"AHJ.DI::GJ*FJJZRMKJ^PL;*S
+MM+6VM[BYNKN\O;Z_P,'"P\3%QL?(R<K+S,W.S]#1TM/4U=;7V-G:V]S=WM_@
+AX>+CY.7FY^CIZNOL[>[O\/'R\_3U]O?X^?K[_/W^_P``
+end
diff --git a/test-cmd/outputs/uuencode/all_bytes.base64 b/test-cmd/outputs/uuencode/all_bytes.base64
@@ -0,0 +1,7 @@
+begin-base64 644 all_bytes
+AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4
+OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3Bx
+cnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmq
+q6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj
+5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==
+====
diff --git a/test-cmd/uuencode.sh b/test-cmd/uuencode.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: MPL-2.0
+
+WD="$(dirname "$0")"
+target="${WD}/../cmd/uuencode"
+plans=6
+. "$(dirname "$0")/tap.sh"
+
+umask 022
+
+t --input='' 'empty' 'empty' 'begin 644 empty
+`
+end
+'
+
+t --input='Cat' 'Cat' '' 'begin 644 -
+#0V%T
+end
+'
+
+t --input='CatCat' 'CatCat' '' 'begin 644 -
+&0V%T0V%T
+end
+'
+
+t --input='CatCatCat' 'CatCatCat' '' 'begin 644 -
+)0V%T0V%T0V%T
+end
+'
+
+t_file all_bytes "$WD/outputs/uuencode/all_bytes" "$WD/inputs/all_bytes" all_bytes
+
+t_file all_bytes.base64 "$WD/outputs/uuencode/all_bytes.base64" -m "$WD/inputs/all_bytes" all_bytes