hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

pem.ha (5814B)


      1 // SPDX-License-Identifier: MPL-2.0
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 use ascii;
      5 use bufio;
      6 use encoding::base64;
      7 use errors;
      8 use fmt;
      9 use io;
     10 use memio;
     11 use os;
     12 use strings;
     13 
     14 
     15 const begin: str = "-----BEGIN ";
     16 const end: str = "-----END ";
     17 const suffix: str = "-----";
     18 
     19 export type decoder = struct {
     20 	in: b64stream,
     21 	label: memio::stream,
     22 	buf: []u8,
     23 };
     24 
     25 export type b64stream = struct {
     26 	stream: io::stream,
     27 	in: bufio::stream,
     28 };
     29 
     30 export type pemdecoder = struct {
     31 	stream: io::stream,
     32 	b64: base64::decoder,
     33 };
     34 
     35 const pemdecoder_vt: io::vtable = io::vtable {
     36 	reader = &pem_read,
     37 	...
     38 };
     39 
     40 const b64stream_r_vt: io::vtable = io::vtable {
     41 	reader = &b64_read,
     42 	...
     43 };
     44 
     45 // Creates a new PEM decoder. The caller must either read it until it returns
     46 // [[io::EOF]], or call [[finish]] to free state associated with the parser.
     47 export fn newdecoder(in: io::handle) decoder = {
     48 	let buf: []u8 = alloc([0...], os::BUFSZ);
     49 	return decoder {
     50 		in = b64stream {
     51 			stream = &b64stream_r_vt,
     52 			in = bufio::init(in, buf, []),
     53 		},
     54 		buf = buf,
     55 		label = memio::dynamic(),
     56 	};
     57 };
     58 
     59 // Frees state associated with this [[decoder]].
     60 export fn finish(dec: *decoder) void = {
     61 	io::close(&dec.label)!;
     62 	free(dec.buf);
     63 };
     64 
     65 // Converts an I/O error returned from a PEM decoder into a human-friendly
     66 // string.
     67 export fn strerror(err: io::error) const str = {
     68 	match (err) {
     69 	case errors::invalid =>
     70 		return "Invalid PEM data";
     71 	case =>
     72 		return io::strerror(err);
     73 	};
     74 };
     75 
     76 // Finds the next PEM boundary in the stream, ignoring any non-PEM data, and
     77 // returns the label and a [[pemdecoder]] from which the encoded data may be
     78 // read, or [[io::EOF]] if no further PEM boundaries are found. The user must
     79 // completely read the pemdecoder until it returns [[io::EOF]] before calling
     80 // [[next]] again.
     81 //
     82 // The label returned by this function is borrowed from the decoder state and
     83 // does not contain "-----BEGIN " or "-----".
     84 export fn next(dec: *decoder) ((str, pemdecoder) | io::EOF | io::error) = {
     85 	for (true) {
     86 		// XXX: This can be improved following
     87 		// https://todo.sr.ht/~sircmpwn/hare/562
     88 		const line = match (bufio::read_line(&dec.in.in)?) {
     89 		case io::EOF =>
     90 			return io::EOF;
     91 		case let line: []u8 =>
     92 			yield match (strings::fromutf8(line)) {
     93 			case let s: str =>
     94 				yield s;
     95 			case =>
     96 				return errors::invalid;
     97 			};
     98 		};
     99 		defer free(line);
    100 		const line = strings::rtrim(line, '\r');
    101 
    102 		if (!strings::hasprefix(line, begin)
    103 				|| !strings::hassuffix(line, suffix)) {
    104 			continue;
    105 		};
    106 
    107 		memio::reset(&dec.label);
    108 		const label = strings::sub(line,
    109 			len(begin), len(line) - len(suffix));
    110 		memio::concat(&dec.label, label)!;
    111 
    112 		return (memio::string(&dec.label)!, pemdecoder {
    113 			stream = &pemdecoder_vt,
    114 			b64 = base64::newdecoder(&base64::std_encoding, &dec.in),
    115 		});
    116 	};
    117 };
    118 
    119 fn pem_read(st: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
    120 	// We need to set up two streams. This is the stream which is actually
    121 	// returned to the caller, which calls the base64 decoder against a
    122 	// special stream (b64stream) which trims out whitespace and EOF's on
    123 	// -----END.
    124 	const st = st: *pemdecoder;
    125 	assert(st.stream.reader == &pem_read);
    126 
    127 	match (io::read(&st.b64, buf)?) {
    128 	case let z: size =>
    129 		return z;
    130 	case io::EOF => void;
    131 	};
    132 
    133 	const line = match (bufio::read_line(st.b64.in)?) {
    134 	case io::EOF =>
    135 		return io::EOF;
    136 	case let line: []u8 =>
    137 		yield match (strings::fromutf8(line)) {
    138 		case let s: str =>
    139 			yield s;
    140 		case =>
    141 			return errors::invalid;
    142 		};
    143 	};
    144 	defer free(line);
    145 	const line = strings::rtrim(line, '\r');
    146 
    147 	if (!strings::hasprefix(line, end)
    148 			|| !strings::hassuffix(line, suffix)) {
    149 		return errors::invalid;
    150 	};
    151 
    152 	// XXX: We could verify the trailer matches but the RFC says it's
    153 	// optional.
    154 	return io::EOF;
    155 };
    156 
    157 fn b64_read(st: *io::stream, buf: []u8) (size | io::EOF | io::error) = {
    158 	const st = st: *b64stream;
    159 	assert(st.stream.reader == &b64_read);
    160 
    161 	const z = match (io::read(&st.in, buf)?) {
    162 	case let z: size =>
    163 		yield z;
    164 	case io::EOF =>
    165 		return errors::invalid; // Missing -----END
    166 	};
    167 
    168 	// Trim off whitespace and look for -----END
    169 	let sub = buf[..z];
    170 	for (let i = 0z; i < len(sub); i += 1) {
    171 		if (sub[i] == '-') {
    172 			bufio::unread(&st.in, sub[i..]);
    173 			sub = sub[..i];
    174 			break;
    175 		};
    176 		if (ascii::isspace(sub[i]: rune)) {
    177 			static delete(sub[i]);
    178 			i -= 1;
    179 			continue;
    180 		};
    181 	};
    182 
    183 	if (len(sub) == 0) {
    184 		return io::EOF;
    185 	};
    186 
    187 	return len(sub);
    188 };
    189 
    190 export type pemencoder = struct {
    191 	stream: io::stream,
    192 	out: io::handle,
    193 	b64: base64::encoder,
    194 	label: str,
    195 	buf: [48]u8,
    196 	ln: u8,
    197 };
    198 
    199 const pemencoder_vt: io::vtable = io::vtable {
    200 	writer = &pem_write,
    201 	closer = &pem_wclose,
    202 	...
    203 };
    204 
    205 // Creates a new PEM encoder stream. The stream has to be closed to write the
    206 // trailer.
    207 export fn newencoder(label: str, s: io::handle) (pemencoder | io::error) = {
    208 	fmt::fprintf(s, "{}{}{}\n", begin, label, suffix)?;
    209 	return pemencoder {
    210 		stream = &pemencoder_vt,
    211 		out = s,
    212 		b64 = base64::newencoder(&base64::std_encoding, s),
    213 		label = label,
    214 		...
    215 	};
    216 };
    217 
    218 fn pem_write(s: *io::stream, buf: const []u8) (size | io::error) = {
    219 	let s = s: *pemencoder;
    220 	let buf = buf: []u8;
    221 	if (len(buf) < len(s.buf) - s.ln) {
    222 		s.buf[s.ln..s.ln+len(buf)] = buf[..];
    223 		s.ln += len(buf): u8;
    224 		return len(buf);
    225 	};
    226 	let z = 0z;
    227 	s.buf[s.ln..] = buf[..len(s.buf) - s.ln];
    228 	z += io::writeall(&s.b64, s.buf)?;
    229 	z += io::write(s.out, ['\n'])?;
    230 	buf = buf[len(s.buf) - s.ln..];
    231 	for (len(buf) >= 48; buf = buf[48..]) {
    232 		z += io::writeall(&s.b64, buf[..48])?;
    233 		z += io::write(s.out, ['\n'])?;
    234 	};
    235 	s.ln = len(buf): u8;
    236 	s.buf[..s.ln] = buf;
    237 	return z + s.ln;
    238 };
    239 
    240 fn pem_wclose(s: *io::stream) (void | io::error) = {
    241 	let s = s: *pemencoder;
    242 	io::writeall(&s.b64, s.buf[..s.ln])?;
    243 	io::close(&s.b64)?;
    244 	fmt::fprintf(s.out, "\n{}{}{}\n", end, s.label, suffix)?;
    245 };