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