1use std::fmt::{Display, Write};
17
18use base64ct::{Base64, Base64Unpadded, Encoding};
19use rand::{CryptoRng, RngCore};
20use tor_bytes::EncodeError;
21use tor_error::{internal, Bug};
22
23use crate::parse::keyword::Keyword;
24use crate::parse::tokenize::tag_keywords_ok;
25use crate::types::misc::{Iso8601TimeNoSp, 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 String {
181 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
182 ItemArgument::write_onto(&self.as_str(), out)
183 }
184}
185
186impl<T: ItemArgument + ?Sized> ItemArgument for &'_ T {
187 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
188 <T as ItemArgument>::write_onto(self, out)
189 }
190}
191
192macro_rules! impl_item_argument_as_display { { $( $ty:ty $(,)? )* } => { $(
196 impl ItemArgument for $ty {
197 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
198 let arg = self.to_string();
199 out.add_arg(&arg.as_str());
200 Ok(())
201 }
202 }
203)* } }
204
205impl_item_argument_as_display! { usize, u8, u16, u32, u64, u128 }
206impl_item_argument_as_display! { isize, i8, i16, i32, i64, i128 }
207impl_item_argument_as_display! {Iso8601TimeNoSp}
213impl ItemArgument for Iso8601TimeSp {
214 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
216 let arg = self.to_string();
217 out.args_raw_nonempty(&arg.as_str());
218 Ok(())
219 }
220}
221
222#[cfg(feature = "hs-pow-full")]
223impl ItemArgument for tor_hscrypto::pow::v1::Seed {
224 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
225 let mut seed_bytes = vec![];
226 tor_bytes::Writer::write(&mut seed_bytes, &self)?;
227 out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
228 Ok(())
229 }
230}
231
232#[cfg(feature = "hs-pow-full")]
233impl ItemArgument for tor_hscrypto::pow::v1::Effort {
234 fn write_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
235 out.add_arg(&<Self as Into<u32>>::into(*self));
236 Ok(())
237 }
238}
239
240impl<'n> ItemEncoder<'n> {
241 pub(crate) fn arg(mut self, arg: &dyn ItemArgument) -> Self {
248 self.add_arg(arg);
249 self
250 }
251
252 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
259 let () = arg
260 .write_onto(self)
261 .unwrap_or_else(|err| self.doc.built = Err(err));
262 }
263
264 #[allow(unused)] pub(crate) fn args_raw_string(mut self, args: &dyn Display) -> Self {
272 let args = args.to_string();
273 if !args.is_empty() {
274 self.args_raw_nonempty(&args);
275 }
276 self
277 }
278
279 fn args_raw_nonempty(&mut self, args: &dyn Display) {
281 self.doc.raw(&format_args!(" {}", args));
282 }
283
284 pub(crate) fn object(
292 self,
293 keywords: &str,
294 data: impl tor_bytes::WriteableOnce,
296 ) {
297 use crate::parse::tokenize::object::*;
298
299 self.doc.write_with(|out| {
300 if keywords.is_empty() || !tag_keywords_ok(keywords) {
301 return Err(internal!("bad object keywords string {:?}", keywords));
302 }
303 let data = {
304 let mut bytes = vec![];
305 data.write_into(&mut bytes)?;
306 Base64::encode_string(&bytes)
307 };
308 let mut data = &data[..];
309 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
310 while !data.is_empty() {
311 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
312 data.split_at(BASE64_PEM_MAX_LINE)
313 } else {
314 (data, "")
315 };
316 writeln!(out, "{l}").expect("write!");
317 data = r;
318 }
319 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
321 Ok(())
322 });
323 }
324}
325
326impl Drop for ItemEncoder<'_> {
327 fn drop(&mut self) {
328 self.doc.raw(&'\n');
329 }
330}
331
332pub trait NetdocBuilder {
334 fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
336}
337
338#[cfg(test)]
339mod test {
340 #![allow(clippy::bool_assert_comparison)]
342 #![allow(clippy::clone_on_copy)]
343 #![allow(clippy::dbg_macro)]
344 #![allow(clippy::mixed_attributes_style)]
345 #![allow(clippy::print_stderr)]
346 #![allow(clippy::print_stdout)]
347 #![allow(clippy::single_char_pattern)]
348 #![allow(clippy::unwrap_used)]
349 #![allow(clippy::unchecked_duration_subtraction)]
350 #![allow(clippy::useless_vec)]
351 #![allow(clippy::needless_pass_by_value)]
352 use super::*;
354 use std::str::FromStr;
355
356 use base64ct::{Base64Unpadded, Encoding};
357
358 #[test]
359 fn time_formats_as_args() {
360 use crate::doc::authcert::AuthCertKwd as ACK;
361 use crate::doc::netstatus::NetstatusKwd as NK;
362
363 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
364 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
365
366 let mut encode = NetdocEncoder::new();
367 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
368 encode
369 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
370 .arg(&"3")
371 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
372 .arg(&t_no_sp);
373
374 let doc = encode.finish().unwrap();
375 println!("{}", doc);
376 assert_eq!(
377 doc,
378 r"dir-key-expires 2020-04-18 08:36:57
379shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
380"
381 );
382 }
383
384 #[test]
385 fn authcert() {
386 use crate::doc::authcert::AuthCertKwd as ACK;
387 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
388
389 let pk_rsa = {
391 let pem = "
392MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
393PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
394qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
395 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
396 };
397
398 let mut encode = NetdocEncoder::new();
399 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
400 encode
401 .item(ACK::FINGERPRINT)
402 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
403 encode
404 .item(ACK::DIR_KEY_PUBLISHED)
405 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
406 encode
407 .item(ACK::DIR_KEY_EXPIRES)
408 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
409 encode
410 .item(ACK::DIR_IDENTITY_KEY)
411 .object("RSA PUBLIC KEY", &*pk_rsa);
412 encode
413 .item(ACK::DIR_SIGNING_KEY)
414 .object("RSA PUBLIC KEY", &*pk_rsa);
415 encode
416 .item(ACK::DIR_KEY_CROSSCERT)
417 .object("ID SIGNATURE", []);
418 encode
419 .item(ACK::DIR_KEY_CERTIFICATION)
420 .object("SIGNATURE", []);
421
422 let doc = encode.finish().unwrap();
423 eprintln!("{}", doc);
424 assert_eq!(
425 doc,
426 r"dir-key-certificate-version 3
427fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
428dir-key-published 2020-04-18 08:36:57
429dir-key-expires 2021-04-18 08:36:57
430dir-identity-key
431-----BEGIN RSA PUBLIC KEY-----
432MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
433PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
434qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
435-----END RSA PUBLIC KEY-----
436dir-signing-key
437-----BEGIN RSA PUBLIC KEY-----
438MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
439PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
440qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
441-----END RSA PUBLIC KEY-----
442dir-key-crosscert
443-----BEGIN ID SIGNATURE-----
444-----END ID SIGNATURE-----
445dir-key-certification
446-----BEGIN SIGNATURE-----
447-----END SIGNATURE-----
448"
449 );
450
451 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
452 }
453}