hare

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

pem.ha (5601B)


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