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::KeywordEncodable;
24use crate::parse::tokenize::tag_keywords_ok;
25use crate::types::misc::Iso8601TimeSp;
26
27#[derive(Debug, Clone)]
32pub struct NetdocEncoder {
33 built: Result<String, Bug>,
40}
41
42#[derive(Debug)]
46pub struct ItemEncoder<'n> {
47 doc: &'n mut NetdocEncoder,
51}
52
53#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
61pub struct Cursor {
62 offset: usize,
66}
67
68pub trait ItemArgument {
75 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
85}
86
87impl NetdocEncoder {
88 pub fn new() -> Self {
90 NetdocEncoder {
91 built: Ok(String::new()),
92 }
93 }
94
95 pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
100 self.raw(&keyword.to_str());
101 ItemEncoder { doc: self }
102 }
103
104 fn raw(&mut self, s: &dyn Display) {
106 self.write_with(|b| {
107 write!(b, "{}", s).expect("write! failed on String");
108 Ok(())
109 });
110 }
111
112 fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
117 let Ok(build) = &mut self.built else {
118 return;
119 };
120 match f(build) {
121 Ok(()) => (),
122 Err(e) => {
123 self.built = Err(e);
124 }
125 }
126 }
127
128 pub fn push_raw_string(&mut self, s: &dyn Display) {
139 self.raw(s);
140 }
141
142 pub 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 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 fn finish(self) -> Result<String, Bug> {
164 self.built
165 }
166}
167
168impl Default for NetdocEncoder {
169 fn default() -> Self {
170 NetdocEncoder::new()
172 }
173}
174
175impl ItemArgument for str {
176 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
177 if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
180 return Err(internal!("invalid keyword argument syntax {:?}", self));
181 }
182 out.args_raw_nonempty(&self);
183 Ok(())
184 }
185}
186
187impl ItemArgument for &str {
188 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
189 <str as ItemArgument>::write_arg_onto(self, out)
190 }
191}
192
193impl<T: crate::NormalItemArgument> ItemArgument for T {
194 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
195 let arg = self.to_string();
196 out.add_arg(&arg.as_str());
197 Ok(())
198 }
199}
200
201impl ItemArgument for Iso8601TimeSp {
202 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
204 let arg = self.to_string();
205 out.args_raw_nonempty(&arg.as_str());
206 Ok(())
207 }
208}
209
210#[cfg(feature = "hs-pow-full")]
211impl ItemArgument for tor_hscrypto::pow::v1::Seed {
212 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
213 let mut seed_bytes = vec![];
214 tor_bytes::Writer::write(&mut seed_bytes, &self)?;
215 out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
216 Ok(())
217 }
218}
219
220#[cfg(feature = "hs-pow-full")]
221impl ItemArgument for tor_hscrypto::pow::v1::Effort {
222 fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
223 out.add_arg(&<Self as Into<u32>>::into(*self));
224 Ok(())
225 }
226}
227
228impl<'n> ItemEncoder<'n> {
229 pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
236 self.add_arg(arg);
237 self
238 }
239
240 pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
247 let () = arg
248 .write_arg_onto(self)
249 .unwrap_or_else(|err| self.doc.built = Err(err));
250 }
251
252 #[allow(unused)] pub(crate) fn args_raw_string(mut self, args: &dyn Display) -> Self {
260 let args = args.to_string();
261 if !args.is_empty() {
262 self.args_raw_nonempty(&args);
263 }
264 self
265 }
266
267 fn args_raw_nonempty(&mut self, args: &dyn Display) {
269 self.doc.raw(&format_args!(" {}", args));
270 }
271
272 pub fn object(
280 self,
281 keywords: &str,
282 data: impl tor_bytes::WriteableOnce,
284 ) {
285 use crate::parse::tokenize::object::*;
286
287 self.doc.write_with(|out| {
288 if keywords.is_empty() || !tag_keywords_ok(keywords) {
289 return Err(internal!("bad object keywords string {:?}", keywords));
290 }
291 let data = {
292 let mut bytes = vec![];
293 data.write_into(&mut bytes)?;
294 Base64::encode_string(&bytes)
295 };
296 let mut data = &data[..];
297 writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
298 while !data.is_empty() {
299 let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
300 data.split_at(BASE64_PEM_MAX_LINE)
301 } else {
302 (data, "")
303 };
304 writeln!(out, "{l}").expect("write!");
305 data = r;
306 }
307 write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
309 Ok(())
310 });
311 }
312}
313
314impl Drop for ItemEncoder<'_> {
315 fn drop(&mut self) {
316 self.doc.raw(&'\n');
317 }
318}
319
320pub 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_time_subtraction)]
350 #![allow(clippy::useless_vec)]
351 #![allow(clippy::needless_pass_by_value)]
352 use super::*;
354 use std::str::FromStr;
355
356 use crate::types::misc::Iso8601TimeNoSp;
357 use base64ct::{Base64Unpadded, Encoding};
358
359 #[test]
360 fn time_formats_as_args() {
361 use crate::doc::authcert::AuthCertKwd as ACK;
362 use crate::doc::netstatus::NetstatusKwd as NK;
363
364 let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
365 let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
366
367 let mut encode = NetdocEncoder::new();
368 encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
369 encode
370 .item(NK::SHARED_RAND_PREVIOUS_VALUE)
371 .arg(&"3")
372 .arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
373 .arg(&t_no_sp);
374
375 let doc = encode.finish().unwrap();
376 println!("{}", doc);
377 assert_eq!(
378 doc,
379 r"dir-key-expires 2020-04-18 08:36:57
380shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
381"
382 );
383 }
384
385 #[test]
386 fn authcert() {
387 use crate::doc::authcert::AuthCertKwd as ACK;
388 use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
389
390 let pk_rsa = {
392 let pem = "
393MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
394PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
395qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
396 Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
397 };
398
399 let mut encode = NetdocEncoder::new();
400 encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
401 encode
402 .item(ACK::FINGERPRINT)
403 .arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
404 encode
405 .item(ACK::DIR_KEY_PUBLISHED)
406 .arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
407 encode
408 .item(ACK::DIR_KEY_EXPIRES)
409 .arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
410 encode
411 .item(ACK::DIR_IDENTITY_KEY)
412 .object("RSA PUBLIC KEY", &*pk_rsa);
413 encode
414 .item(ACK::DIR_SIGNING_KEY)
415 .object("RSA PUBLIC KEY", &*pk_rsa);
416 encode
417 .item(ACK::DIR_KEY_CROSSCERT)
418 .object("ID SIGNATURE", []);
419 encode
420 .item(ACK::DIR_KEY_CERTIFICATION)
421 .object("SIGNATURE", []);
422
423 let doc = encode.finish().unwrap();
424 eprintln!("{}", doc);
425 assert_eq!(
426 doc,
427 r"dir-key-certificate-version 3
428fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
429dir-key-published 2020-04-18 08:36:57
430dir-key-expires 2021-04-18 08:36:57
431dir-identity-key
432-----BEGIN RSA PUBLIC KEY-----
433MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
434PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
435qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
436-----END RSA PUBLIC KEY-----
437dir-signing-key
438-----BEGIN RSA PUBLIC KEY-----
439MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
440PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
441qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
442-----END RSA PUBLIC KEY-----
443dir-key-crosscert
444-----BEGIN ID SIGNATURE-----
445-----END ID SIGNATURE-----
446dir-key-certification
447-----BEGIN SIGNATURE-----
448-----END SIGNATURE-----
449"
450 );
451
452 let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
453 }
454}