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 };