1mod multiplicity;
17#[macro_use]
18mod derive;
19
20use std::cmp;
21use std::collections::BTreeSet;
22use std::fmt::Write;
23use std::iter;
24use std::marker::PhantomData;
25
26use base64ct::{Base64, Base64Unpadded, Encoding};
27use educe::Educe;
28use itertools::Itertools;
29use paste::paste;
30use rand::{CryptoRng, RngCore};
31use tor_bytes::EncodeError;
32use tor_error::internal;
33use void::Void;
34
35use crate::KeywordEncodable;
36use crate::parse::tokenize::tag_keywords_ok;
37use crate::types::misc::Iso8601TimeSp;
38
39#[doc(hidden)]
41pub use {
42 derive::{DisplayHelper, RestMustComeLastMarker},
43 multiplicity::{
44 MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
45 SingletonMultiplicitySelector,
46 },
47 std::fmt::{self, Display},
48 std::result::Result,
49 tor_error::{Bug, into_internal},
50};
51
52#[derive(Debug, Clone)]
57pub struct NetdocEncoder {
58 built: Result<String, Bug>,
65}
66
67#[derive(Debug)]
71pub struct ItemEncoder<'n> {
72 doc: &'n mut NetdocEncoder,
76}
77
78#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
86pub struct Cursor {
87 offset: usize,
91}
92
93pub trait ItemArgument {
102 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
112}
113
114impl NetdocEncoder {
115 pub fn new() -> Self {
117 NetdocEncoder {
118 built: Ok(String::new()),
119 }
120 }
121
122 pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
127 self.raw(&keyword.to_str());
128 ItemEncoder { doc: self }
129 }
130
131 fn raw(&mut self, s: &dyn Display) {
133 self.write_with(|b| {
134 write!(b, "{}", s).expect("write! failed on String");
135 Ok(())
136 });
137 }
138
139 fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
144 let Ok(build) = &mut self.built else {
145 return;
146 };
147 match f(build) {
148 Ok(()) => (),
149 Err(e) => {
150 self.built = Err(e);
151 }
152 }
153 }
154
155 pub fn push_raw_string(&mut self, s: &dyn Display) {
166 self.raw(s);
167 }
168
169 pub fn cursor(&self) -> Cursor {
171 let offset = match &self.built {
172 Ok(b) => b.len(),
173 Err(_) => usize::MAX,
174 };
175 Cursor { offset }
176 }
177
178 pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
182 self.built
183 .as_ref()
184 .map_err(Clone::clone)?
185 .get(begin.offset..end.offset)
186 .ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
187 }
188
189 pub fn finish(self) -> Result<String, Bug> {
191 self.built
192 }
193}
194
195impl Default for NetdocEncoder {
196 fn default() -> Self {
197 NetdocEncoder::new()
199 }
200}
201
202impl ItemArgument for str {
203 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
204 if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
207 return Err(internal!(
208 "invalid netdoc keyword line argument syntax {:?}",
209 self
210 ));
211 }
212 out.args_raw_nonempty(&self);
213 Ok(())
214 }
215}
216
217impl ItemArgument for &str {
218 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
219 <str as ItemArgument>::write_arg_onto(self, out)
220 }
221}
222
223impl<T: crate::NormalItemArgument> ItemArgument for T {
224 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225 (*self.to_string()).write_arg_onto(out)
226 }
227}
228
229impl ItemArgument for Iso8601TimeSp {
230 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
232 let arg = self.to_string();
233 out.args_raw_nonempty(&arg.as_str());
234 Ok(())
235 }
236}
237
238#[cfg(feature = "hs-pow-full")]
239impl ItemArgument for tor_hscrypto::pow::v1::Seed {
240 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
241 let mut seed_bytes = vec![];
242 tor_bytes::Writer::write(&mut seed_bytes, &self)?;
243 out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
244 Ok(())
245 }
246}
247
248#[cfg(feature = "hs-pow-full")]
249impl ItemArgument for tor_hscrypto::pow::v1::Effort {
250 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
251 out.add_arg(&<Self as Into<u32>>::into(*self));
252 Ok(())
253 }
254}
255
256impl<'n> ItemEncoder<'n> {
257 pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
267 self.add_arg(arg);
268 self
269 }
270
271 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
278 let () = arg
279 .write_arg_onto(self)
280 .unwrap_or_else(|err| self.doc.built = Err(err));
281 }
282
283 pub fn args_raw_string(&mut self, args: &dyn Display) {
290 let args = args.to_string();
291 if !args.is_empty() {
292 self.args_raw_nonempty(&args);
293 }
294 }
295
296 fn args_raw_nonempty(&mut self, args: &dyn Display) {
298 self.doc.raw(&format_args!(" {}", args));
299 }
300
301 pub fn object(
309 self,
310 keywords: &str,
311 data: impl tor_bytes::WriteableOnce,
313 ) {
314 use crate::parse::tokenize::object::*;
315
316 self.doc.write_with(|out| {
317 if keywords.is_empty() || !tag_keywords_ok(keywords) {
318 return Err(internal!("bad object keywords string {:?}", keywords));
319 }
320 let data = {
321 let mut bytes = vec![];
322 data.write_into(&mut bytes)?;
323 Base64::encode_string(&bytes)
324 };
325 let mut data = &data[..];
326 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
327 while !data.is_empty() {
328 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
329 data.split_at(BASE64_PEM_MAX_LINE)
330 } else {
331 (data, "")
332 };
333 writeln!(out, "{l}").expect("write!");
334 data = r;
335 }
336 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
338 Ok(())
339 });
340 }
341
342 pub fn finish(self) {}
346}
347
348impl Drop for ItemEncoder<'_> {
349 fn drop(&mut self) {
350 self.doc.raw(&'\n');
351 }
352}
353
354pub trait EncodeOrd {
361 fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
365}
366impl<T: Ord> EncodeOrd for T {
367 fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
368 self.cmp(other)
369 }
370}
371
372pub trait NetdocEncodable {
374 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
376}
377
378pub trait NetdocEncodableFields {
382 fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
384}
385
386pub trait ItemValueEncodable {
388 fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
392}
393
394pub trait ItemObjectEncodable {
396 fn label(&self) -> &str;
398
399 fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
406}
407
408pub trait NetdocBuilder {
422 fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
424}
425
426impl ItemValueEncodable for Void {
427 fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
428 void::unreachable(*self)
429 }
430}
431
432impl ItemObjectEncodable for Void {
433 fn label(&self) -> &str {
434 void::unreachable(*self)
435 }
436 fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
437 void::unreachable(*self)
438 }
439}
440
441macro_rules! item_value_encodable_for_tuple {
443 { $($i:literal)* } => { paste! {
444 impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
445 fn write_item_value_onto(
446 &self,
447 #[allow(unused)]
448 mut out: ItemEncoder,
449 ) -> Result<(), Bug> {
450 $(
451 <[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
452 )*
453 Ok(())
454 }
455 }
456 } }
457}
458
459item_value_encodable_for_tuple! {}
460item_value_encodable_for_tuple! { 0 }
461item_value_encodable_for_tuple! { 0 1 }
462item_value_encodable_for_tuple! { 0 1 2 }
463item_value_encodable_for_tuple! { 0 1 2 3 }
464item_value_encodable_for_tuple! { 0 1 2 3 4 }
465item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
466item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
467item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
468item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
469item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
470
471#[cfg(test)]
472mod test {
473 #![allow(clippy::bool_assert_comparison)]
475 #![allow(clippy::clone_on_copy)]
476 #![allow(clippy::dbg_macro)]
477 #![allow(clippy::mixed_attributes_style)]
478 #![allow(clippy::print_stderr)]
479 #![allow(clippy::print_stdout)]
480 #![allow(clippy::single_char_pattern)]
481 #![allow(clippy::unwrap_used)]
482 #![allow(clippy::unchecked_time_subtraction)]
483 #![allow(clippy::useless_vec)]
484 #![allow(clippy::needless_pass_by_value)]
485 use super::*;
487 use std::str::FromStr;
488
489 use crate::types::misc::Iso8601TimeNoSp;
490 use base64ct::{Base64Unpadded, Encoding};
491
492 #[test]
493 fn time_formats_as_args() {
494 use crate::doc::authcert::AuthCertKwd as ACK;
495 use crate::doc::netstatus::NetstatusKwd as NK;
496
497 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
498 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
499
500 let mut encode = NetdocEncoder::new();
501 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
502 encode
503 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
504 .arg(&"3")
505 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
506 .arg(&t_no_sp);
507
508 let doc = encode.finish().unwrap();
509 println!("{}", doc);
510 assert_eq!(
511 doc,
512 r"dir-key-expires 2020-04-18 08:36:57
513shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
514"
515 );
516 }
517
518 #[test]
519 fn authcert() {
520 use crate::doc::authcert::AuthCertKwd as ACK;
521 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
522
523 let pk_rsa = {
525 let pem = "
526MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
527PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
528qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
529 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
530 };
531
532 let mut encode = NetdocEncoder::new();
533 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
534 encode
535 .item(ACK::FINGERPRINT)
536 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
537 encode
538 .item(ACK::DIR_KEY_PUBLISHED)
539 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
540 encode
541 .item(ACK::DIR_KEY_EXPIRES)
542 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
543 encode
544 .item(ACK::DIR_IDENTITY_KEY)
545 .object("RSA PUBLIC KEY", &*pk_rsa);
546 encode
547 .item(ACK::DIR_SIGNING_KEY)
548 .object("RSA PUBLIC KEY", &*pk_rsa);
549 encode
550 .item(ACK::DIR_KEY_CROSSCERT)
551 .object("ID SIGNATURE", []);
552 encode
553 .item(ACK::DIR_KEY_CERTIFICATION)
554 .object("SIGNATURE", []);
555
556 let doc = encode.finish().unwrap();
557 eprintln!("{}", doc);
558 assert_eq!(
559 doc,
560 r"dir-key-certificate-version 3
561fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
562dir-key-published 2020-04-18 08:36:57
563dir-key-expires 2021-04-18 08:36:57
564dir-identity-key
565-----BEGIN RSA PUBLIC KEY-----
566MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
567PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
568qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
569-----END RSA PUBLIC KEY-----
570dir-signing-key
571-----BEGIN RSA PUBLIC KEY-----
572MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
573PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
574qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
575-----END RSA PUBLIC KEY-----
576dir-key-crosscert
577-----BEGIN ID SIGNATURE-----
578-----END ID SIGNATURE-----
579dir-key-certification
580-----BEGIN SIGNATURE-----
581-----END SIGNATURE-----
582"
583 );
584
585 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
586 }
587}