1use std::fmt::{Display, Write};
17
18use base64ct::{Base64, Base64Unpadded, Encoding};
19use rand::{CryptoRng, RngCore};
20use tor_bytes::EncodeError;
21use tor_error::{Bug, internal};
22
23use crate::parse::keyword::Keyword;
24use crate::parse::tokenize::tag_keywords_ok;
25use crate::types::misc::Iso8601TimeSp;
26
27#[derive(Debug, Clone)]
32pub(crate) struct NetdocEncoder {
33 built: Result<String, Bug>,
39}
40
41#[derive(Debug)]
45pub(crate) struct ItemEncoder<'n> {
46 doc: &'n mut NetdocEncoder,
50}
51
52#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
60pub(crate) struct Cursor {
61 offset: usize,
65}
66
67pub(crate) trait ItemArgument {
74 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
84}
85
86impl NetdocEncoder {
87 pub(crate) fn new() -> Self {
89 NetdocEncoder {
90 built: Ok(String::new()),
91 }
92 }
93
94 pub(crate) fn item(&mut self, keyword: impl Keyword) -> ItemEncoder {
99 self.raw(&keyword.to_str());
100 ItemEncoder { doc: self }
101 }
102
103 fn raw(&mut self, s: &dyn Display) {
105 self.write_with(|b| {
106 write!(b, "{}", s).expect("write! failed on String");
107 Ok(())
108 });
109 }
110
111 fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
116 let Ok(build) = &mut self.built else {
117 return;
118 };
119 match f(build) {
120 Ok(()) => (),
121 Err(e) => {
122 self.built = Err(e);
123 }
124 }
125 }
126
127 #[allow(dead_code)] pub(crate) fn push_raw_string(&mut self, s: &dyn Display) {
139 self.raw(s);
140 }
141
142 pub(crate) fn cursor(&self) -> Cursor {
144 let offset = match &self.built {
145 Ok(b) => b.len(),
146 Err(_) => usize::MAX,
147 };
148 Cursor { offset }
149 }
150
151 pub(crate) fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
155 self.built
156 .as_ref()
157 .map_err(Clone::clone)?
158 .get(begin.offset..end.offset)
159 .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
160 }
161
162 pub(crate) fn finish(self) -> Result<String, Bug> {
164 self.built
165 }
166}
167
168impl ItemArgument for str {
169 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
170 if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
173 return Err(internal!("invalid keyword argument syntax {:?}", self));
174 }
175 out.args_raw_nonempty(&self);
176 Ok(())
177 }
178}
179
180impl ItemArgument for &str {
181 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182 <str as ItemArgument>::write_onto(self, out)
183 }
184}
185
186impl<T: crate::NormalItemArgument> ItemArgument for T {
187 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
188 let arg = self.to_string();
189 out.add_arg(&arg.as_str());
190 Ok(())
191 }
192}
193
194impl ItemArgument for Iso8601TimeSp {
195 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
197 let arg = self.to_string();
198 out.args_raw_nonempty(&arg.as_str());
199 Ok(())
200 }
201}
202
203#[cfg(feature = "hs-pow-full")]
204impl ItemArgument for tor_hscrypto::pow::v1::Seed {
205 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
206 let mut seed_bytes = vec![];
207 tor_bytes::Writer::write(&mut seed_bytes, &self)?;
208 out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
209 Ok(())
210 }
211}
212
213#[cfg(feature = "hs-pow-full")]
214impl ItemArgument for tor_hscrypto::pow::v1::Effort {
215 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
216 out.add_arg(&<Self as Into<u32>>::into(*self));
217 Ok(())
218 }
219}
220
221impl<'n> ItemEncoder<'n> {
222 pub(crate) fn arg(mut self, arg: &dyn ItemArgument) -> Self {
229 self.add_arg(arg);
230 self
231 }
232
233 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
240 let () = arg
241 .write_onto(self)
242 .unwrap_or_else(|err| self.doc.built = Err(err));
243 }
244
245 #[allow(unused)] pub(crate) fn args_raw_string(mut self, args: &dyn Display) -> Self {
253 let args = args.to_string();
254 if !args.is_empty() {
255 self.args_raw_nonempty(&args);
256 }
257 self
258 }
259
260 fn args_raw_nonempty(&mut self, args: &dyn Display) {
262 self.doc.raw(&format_args!(" {}", args));
263 }
264
265 pub(crate) fn object(
273 self,
274 keywords: &str,
275 data: impl tor_bytes::WriteableOnce,
277 ) {
278 use crate::parse::tokenize::object::*;
279
280 self.doc.write_with(|out| {
281 if keywords.is_empty() || !tag_keywords_ok(keywords) {
282 return Err(internal!("bad object keywords string {:?}", keywords));
283 }
284 let data = {
285 let mut bytes = vec![];
286 data.write_into(&mut bytes)?;
287 Base64::encode_string(&bytes)
288 };
289 let mut data = &data[..];
290 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
291 while !data.is_empty() {
292 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
293 data.split_at(BASE64_PEM_MAX_LINE)
294 } else {
295 (data, "")
296 };
297 writeln!(out, "{l}").expect("write!");
298 data = r;
299 }
300 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
302 Ok(())
303 });
304 }
305}
306
307impl Drop for ItemEncoder<'_> {
308 fn drop(&mut self) {
309 self.doc.raw(&'\n');
310 }
311}
312
313pub trait NetdocBuilder {
315 fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
317}
318
319#[cfg(test)]
320mod test {
321 #![allow(clippy::bool_assert_comparison)]
323 #![allow(clippy::clone_on_copy)]
324 #![allow(clippy::dbg_macro)]
325 #![allow(clippy::mixed_attributes_style)]
326 #![allow(clippy::print_stderr)]
327 #![allow(clippy::print_stdout)]
328 #![allow(clippy::single_char_pattern)]
329 #![allow(clippy::unwrap_used)]
330 #![allow(clippy::unchecked_duration_subtraction)]
331 #![allow(clippy::useless_vec)]
332 #![allow(clippy::needless_pass_by_value)]
333 use super::*;
335 use std::str::FromStr;
336
337 use crate::types::misc::Iso8601TimeNoSp;
338 use base64ct::{Base64Unpadded, Encoding};
339
340 #[test]
341 fn time_formats_as_args() {
342 use crate::doc::authcert::AuthCertKwd as ACK;
343 use crate::doc::netstatus::NetstatusKwd as NK;
344
345 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
346 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
347
348 let mut encode = NetdocEncoder::new();
349 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
350 encode
351 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
352 .arg(&"3")
353 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
354 .arg(&t_no_sp);
355
356 let doc = encode.finish().unwrap();
357 println!("{}", doc);
358 assert_eq!(
359 doc,
360 r"dir-key-expires 2020-04-18 08:36:57
361shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
362"
363 );
364 }
365
366 #[test]
367 fn authcert() {
368 use crate::doc::authcert::AuthCertKwd as ACK;
369 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
370
371 let pk_rsa = {
373 let pem = "
374MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
375PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
376qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
377 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
378 };
379
380 let mut encode = NetdocEncoder::new();
381 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
382 encode
383 .item(ACK::FINGERPRINT)
384 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
385 encode
386 .item(ACK::DIR_KEY_PUBLISHED)
387 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
388 encode
389 .item(ACK::DIR_KEY_EXPIRES)
390 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
391 encode
392 .item(ACK::DIR_IDENTITY_KEY)
393 .object("RSA PUBLIC KEY", &*pk_rsa);
394 encode
395 .item(ACK::DIR_SIGNING_KEY)
396 .object("RSA PUBLIC KEY", &*pk_rsa);
397 encode
398 .item(ACK::DIR_KEY_CROSSCERT)
399 .object("ID SIGNATURE", []);
400 encode
401 .item(ACK::DIR_KEY_CERTIFICATION)
402 .object("SIGNATURE", []);
403
404 let doc = encode.finish().unwrap();
405 eprintln!("{}", doc);
406 assert_eq!(
407 doc,
408 r"dir-key-certificate-version 3
409fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
410dir-key-published 2020-04-18 08:36:57
411dir-key-expires 2021-04-18 08:36:57
412dir-identity-key
413-----BEGIN RSA PUBLIC KEY-----
414MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
415PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
416qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
417-----END RSA PUBLIC KEY-----
418dir-signing-key
419-----BEGIN RSA PUBLIC KEY-----
420MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
421PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
422qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
423-----END RSA PUBLIC KEY-----
424dir-key-crosscert
425-----BEGIN ID SIGNATURE-----
426-----END ID SIGNATURE-----
427dir-key-certification
428-----BEGIN SIGNATURE-----
429-----END SIGNATURE-----
430"
431 );
432
433 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
434 }
435}