1mod multiplicity;
40#[macro_use]
41mod derive;
42
43use std::cmp;
44use std::collections::BTreeSet;
45use std::fmt::Write;
46use std::iter;
47use std::marker::PhantomData;
48
49use base64ct::{Base64, Base64Unpadded, Encoding};
50use educe::Educe;
51use itertools::Itertools;
52use paste::paste;
53use rand::{CryptoRng, RngCore};
54use tor_bytes::EncodeError;
55use tor_error::internal;
56use void::Void;
57
58use crate::KeywordEncodable;
59use crate::parse::tokenize::tag_keywords_ok;
60use crate::types::misc::Iso8601TimeSp;
61
62#[doc(hidden)]
64pub use {
65 derive::{DisplayHelper, RestMustComeLastMarker},
66 multiplicity::{
67 MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
68 SingletonMultiplicitySelector,
69 },
70 std::fmt::{self, Display},
71 std::result::Result,
72 tor_error::{Bug, into_internal},
73};
74
75#[derive(Debug, Clone)]
80pub struct NetdocEncoder {
81 built: Result<String, Bug>,
88}
89
90#[derive(Debug)]
94pub struct ItemEncoder<'n> {
95 doc: &'n mut NetdocEncoder,
99}
100
101#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
109pub struct Cursor {
110 offset: usize,
114}
115
116pub trait ItemArgument {
125 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
135}
136
137impl NetdocEncoder {
138 pub fn new() -> Self {
140 NetdocEncoder {
141 built: Ok(String::new()),
142 }
143 }
144
145 pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
150 self.raw(&keyword.to_str());
151 ItemEncoder { doc: self }
152 }
153
154 fn raw(&mut self, s: &dyn Display) {
156 self.write_with(|b| {
157 write!(b, "{}", s).expect("write! failed on String");
158 Ok(())
159 });
160 }
161
162 fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
167 let Ok(build) = &mut self.built else {
168 return;
169 };
170 match f(build) {
171 Ok(()) => (),
172 Err(e) => {
173 self.built = Err(e);
174 }
175 }
176 }
177
178 pub fn push_raw_string(&mut self, s: &dyn Display) {
189 self.raw(s);
190 }
191
192 pub fn cursor(&self) -> Cursor {
194 let offset = match &self.built {
195 Ok(b) => b.len(),
196 Err(_) => usize::MAX,
197 };
198 Cursor { offset }
199 }
200
201 pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
205 self.built
206 .as_ref()
207 .map_err(Clone::clone)?
208 .get(begin.offset..end.offset)
209 .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
210 }
211
212 pub fn finish(self) -> Result<String, Bug> {
214 self.built
215 }
216}
217
218impl Default for NetdocEncoder {
219 fn default() -> Self {
220 NetdocEncoder::new()
222 }
223}
224
225impl ItemArgument for str {
226 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
227 if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
230 return Err(internal!(
231 "invalid netdoc keyword line argument syntax {:?}",
232 self
233 ));
234 }
235 out.args_raw_nonempty(&self);
236 Ok(())
237 }
238}
239
240impl ItemArgument for &str {
241 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
242 <str as ItemArgument>::write_arg_onto(self, out)
243 }
244}
245
246impl<T: crate::NormalItemArgument> ItemArgument for T {
247 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
248 (*self.to_string()).write_arg_onto(out)
249 }
250}
251
252impl ItemArgument for Iso8601TimeSp {
253 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
255 let arg = self.to_string();
256 out.args_raw_nonempty(&arg.as_str());
257 Ok(())
258 }
259}
260
261#[cfg(feature = "hs-pow-full")]
262impl ItemArgument for tor_hscrypto::pow::v1::Seed {
263 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
264 let mut seed_bytes = vec![];
265 tor_bytes::Writer::write(&mut seed_bytes, &self)?;
266 out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
267 Ok(())
268 }
269}
270
271#[cfg(feature = "hs-pow-full")]
272impl ItemArgument for tor_hscrypto::pow::v1::Effort {
273 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
274 out.add_arg(&<Self as Into<u32>>::into(*self));
275 Ok(())
276 }
277}
278
279impl<'n> ItemEncoder<'n> {
280 pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
290 self.add_arg(arg);
291 self
292 }
293
294 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
301 let () = arg
302 .write_arg_onto(self)
303 .unwrap_or_else(|err| self.doc.built = Err(err));
304 }
305
306 pub fn args_raw_string(&mut self, args: &dyn Display) {
313 let args = args.to_string();
314 if !args.is_empty() {
315 self.args_raw_nonempty(&args);
316 }
317 }
318
319 fn args_raw_nonempty(&mut self, args: &dyn Display) {
321 self.doc.raw(&format_args!(" {}", args));
322 }
323
324 pub fn object(
332 self,
333 keywords: &str,
334 data: impl tor_bytes::WriteableOnce,
336 ) {
337 use crate::parse::tokenize::object::*;
338
339 self.doc.write_with(|out| {
340 if keywords.is_empty() || !tag_keywords_ok(keywords) {
341 return Err(internal!("bad object keywords string {:?}", keywords));
342 }
343 let data = {
344 let mut bytes = vec![];
345 data.write_into(&mut bytes)?;
346 Base64::encode_string(&bytes)
347 };
348 let mut data = &data[..];
349 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
350 while !data.is_empty() {
351 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
352 data.split_at(BASE64_PEM_MAX_LINE)
353 } else {
354 (data, "")
355 };
356 writeln!(out, "{l}").expect("write!");
357 data = r;
358 }
359 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
361 Ok(())
362 });
363 }
364
365 pub fn finish(self) {}
369}
370
371impl Drop for ItemEncoder<'_> {
372 fn drop(&mut self) {
373 self.doc.raw(&'\n');
374 }
375}
376
377pub trait EncodeOrd {
384 fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
388}
389impl<T: Ord> EncodeOrd for T {
390 fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
391 self.cmp(other)
392 }
393}
394
395pub trait NetdocEncodable {
397 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
399}
400
401pub trait NetdocEncodableFields {
405 fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
407}
408
409pub trait ItemValueEncodable {
411 fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
415}
416
417pub trait ItemObjectEncodable {
419 fn label(&self) -> &str;
421
422 fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
429}
430
431pub trait NetdocBuilder {
445 fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
447}
448
449impl ItemValueEncodable for Void {
450 fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
451 void::unreachable(*self)
452 }
453}
454
455impl ItemObjectEncodable for Void {
456 fn label(&self) -> &str {
457 void::unreachable(*self)
458 }
459 fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
460 void::unreachable(*self)
461 }
462}
463
464macro_rules! item_value_encodable_for_tuple {
466 { $($i:literal)* } => { paste! {
467 impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
468 fn write_item_value_onto(
469 &self,
470 #[allow(unused)]
471 mut out: ItemEncoder,
472 ) -> Result<(), Bug> {
473 $(
474 <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
475 )*
476 Ok(())
477 }
478 }
479 } }
480}
481
482item_value_encodable_for_tuple! {}
483item_value_encodable_for_tuple! { 0 }
484item_value_encodable_for_tuple! { 0 1 }
485item_value_encodable_for_tuple! { 0 1 2 }
486item_value_encodable_for_tuple! { 0 1 2 3 }
487item_value_encodable_for_tuple! { 0 1 2 3 4 }
488item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
489item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
490item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
491item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
492item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
493
494#[cfg(test)]
495mod test {
496 #![allow(clippy::bool_assert_comparison)]
498 #![allow(clippy::clone_on_copy)]
499 #![allow(clippy::dbg_macro)]
500 #![allow(clippy::mixed_attributes_style)]
501 #![allow(clippy::print_stderr)]
502 #![allow(clippy::print_stdout)]
503 #![allow(clippy::single_char_pattern)]
504 #![allow(clippy::unwrap_used)]
505 #![allow(clippy::unchecked_time_subtraction)]
506 #![allow(clippy::useless_vec)]
507 #![allow(clippy::needless_pass_by_value)]
508 use super::*;
510 use std::str::FromStr;
511
512 use crate::types::misc::Iso8601TimeNoSp;
513 use base64ct::{Base64Unpadded, Encoding};
514
515 #[test]
516 fn time_formats_as_args() {
517 use crate::doc::authcert::AuthCertKwd as ACK;
518 use crate::doc::netstatus::NetstatusKwd as NK;
519
520 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
521 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
522
523 let mut encode = NetdocEncoder::new();
524 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
525 encode
526 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
527 .arg(&"3")
528 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
529 .arg(&t_no_sp);
530
531 let doc = encode.finish().unwrap();
532 println!("{}", doc);
533 assert_eq!(
534 doc,
535 r"dir-key-expires 2020-04-18 08:36:57
536shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
537"
538 );
539 }
540
541 #[test]
542 fn authcert() {
543 use crate::doc::authcert::AuthCertKwd as ACK;
544 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
545
546 let pk_rsa = {
548 let pem = "
549MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
550PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
551qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
552 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
553 };
554
555 let mut encode = NetdocEncoder::new();
556 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
557 encode
558 .item(ACK::FINGERPRINT)
559 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
560 encode
561 .item(ACK::DIR_KEY_PUBLISHED)
562 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
563 encode
564 .item(ACK::DIR_KEY_EXPIRES)
565 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
566 encode
567 .item(ACK::DIR_IDENTITY_KEY)
568 .object("RSA PUBLIC KEY", &*pk_rsa);
569 encode
570 .item(ACK::DIR_SIGNING_KEY)
571 .object("RSA PUBLIC KEY", &*pk_rsa);
572 encode
573 .item(ACK::DIR_KEY_CROSSCERT)
574 .object("ID SIGNATURE", []);
575 encode
576 .item(ACK::DIR_KEY_CERTIFICATION)
577 .object("SIGNATURE", []);
578
579 let doc = encode.finish().unwrap();
580 eprintln!("{}", doc);
581 assert_eq!(
582 doc,
583 r"dir-key-certificate-version 3
584fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
585dir-key-published 2020-04-18 08:36:57
586dir-key-expires 2021-04-18 08:36:57
587dir-identity-key
588-----BEGIN RSA PUBLIC KEY-----
589MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
590PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
591qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
592-----END RSA PUBLIC KEY-----
593dir-signing-key
594-----BEGIN RSA PUBLIC KEY-----
595MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
596PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
597qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
598-----END RSA PUBLIC KEY-----
599dir-key-crosscert
600-----BEGIN ID SIGNATURE-----
601-----END ID SIGNATURE-----
602dir-key-certification
603-----BEGIN SIGNATURE-----
604-----END SIGNATURE-----
605"
606 );
607
608 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
609 }
610}