mod multiplicity;
#[macro_use]
mod derive;
use std::cmp;
use std::collections::BTreeSet;
use std::fmt::Write;
use std::iter;
use std::marker::PhantomData;
use base64ct::{Base64, Base64Unpadded, Encoding};
use educe::Educe;
use itertools::Itertools;
use paste::paste;
use rand::{CryptoRng, RngCore};
use tor_bytes::EncodeError;
use tor_error::internal;
use void::Void;
use crate::KeywordEncodable;
use crate::parse::tokenize::tag_keywords_ok;
use crate::types::misc::Iso8601TimeSp;
#[doc(hidden)]
pub use {
derive::{DisplayHelper, RestMustComeLastMarker},
multiplicity::{
MultiplicityMethods, MultiplicitySelector, OptionalityMethods,
SingletonMultiplicitySelector,
},
std::fmt::{self, Display},
std::result::Result,
tor_error::{Bug, into_internal},
};
#[derive(Debug, Clone)]
pub struct NetdocEncoder {
built: Result<String, Bug>,
}
#[derive(Debug)]
pub struct ItemEncoder<'n> {
doc: &'n mut NetdocEncoder,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub struct Cursor {
offset: usize,
}
pub trait ItemArgument {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug>;
}
impl NetdocEncoder {
pub fn new() -> Self {
NetdocEncoder {
built: Ok(String::new()),
}
}
pub fn item(&mut self, keyword: impl KeywordEncodable) -> ItemEncoder {
self.raw(&keyword.to_str());
ItemEncoder { doc: self }
}
fn raw(&mut self, s: &dyn Display) {
self.write_with(|b| {
write!(b, "{}", s).expect("write! failed on String");
Ok(())
});
}
fn write_with(&mut self, f: impl FnOnce(&mut String) -> Result<(), Bug>) {
let Ok(build) = &mut self.built else {
return;
};
match f(build) {
Ok(()) => (),
Err(e) => {
self.built = Err(e);
}
}
}
pub fn push_raw_string(&mut self, s: &dyn Display) {
self.raw(s);
}
pub fn cursor(&self) -> Cursor {
let offset = match &self.built {
Ok(b) => b.len(),
Err(_) => usize::MAX,
};
Cursor { offset }
}
pub fn slice(&self, begin: Cursor, end: Cursor) -> Result<&str, Bug> {
self.built
.as_ref()
.map_err(Clone::clone)?
.get(begin.offset..end.offset)
.ok_or_else(|| internal!("NetdocEncoder::slice out of bounds, Cursor mismanaged"))
}
pub fn finish(self) -> Result<String, Bug> {
self.built
}
}
impl Default for NetdocEncoder {
fn default() -> Self {
NetdocEncoder::new()
}
}
impl ItemArgument for str {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
if self.is_empty() || self.chars().any(|c| !c.is_ascii_graphic()) {
return Err(internal!(
"invalid netdoc keyword line argument syntax {:?}",
self
));
}
out.args_raw_nonempty(&self);
Ok(())
}
}
impl ItemArgument for &str {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
<str as ItemArgument>::write_arg_onto(self, out)
}
}
impl<T: crate::NormalItemArgument> ItemArgument for T {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
(*self.to_string()).write_arg_onto(out)
}
}
impl ItemArgument for Iso8601TimeSp {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
let arg = self.to_string();
out.args_raw_nonempty(&arg.as_str());
Ok(())
}
}
#[cfg(feature = "hs-pow-full")]
impl ItemArgument for tor_hscrypto::pow::v1::Seed {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
let mut seed_bytes = vec![];
tor_bytes::Writer::write(&mut seed_bytes, &self)?;
out.add_arg(&Base64Unpadded::encode_string(&seed_bytes));
Ok(())
}
}
#[cfg(feature = "hs-pow-full")]
impl ItemArgument for tor_hscrypto::pow::v1::Effort {
fn write_arg_onto(&self, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
out.add_arg(&<Self as Into<u32>>::into(*self));
Ok(())
}
}
impl<'n> ItemEncoder<'n> {
pub fn arg(mut self, arg: &dyn ItemArgument) -> Self {
self.add_arg(arg);
self
}
pub(crate) fn add_arg(&mut self, arg: &dyn ItemArgument) {
let () = arg
.write_arg_onto(self)
.unwrap_or_else(|err| self.doc.built = Err(err));
}
pub fn args_raw_string(&mut self, args: &dyn Display) {
let args = args.to_string();
if !args.is_empty() {
self.args_raw_nonempty(&args);
}
}
fn args_raw_nonempty(&mut self, args: &dyn Display) {
self.doc.raw(&format_args!(" {}", args));
}
pub fn object(
self,
keywords: &str,
data: impl tor_bytes::WriteableOnce,
) {
use crate::parse::tokenize::object::*;
self.doc.write_with(|out| {
if keywords.is_empty() || !tag_keywords_ok(keywords) {
return Err(internal!("bad object keywords string {:?}", keywords));
}
let data = {
let mut bytes = vec![];
data.write_into(&mut bytes)?;
Base64::encode_string(&bytes)
};
let mut data = &data[..];
writeln!(out, "\n{BEGIN_STR}{keywords}{TAG_END}").expect("write!");
while !data.is_empty() {
let (l, r) = if data.len() > BASE64_PEM_MAX_LINE {
data.split_at(BASE64_PEM_MAX_LINE)
} else {
(data, "")
};
writeln!(out, "{l}").expect("write!");
data = r;
}
write!(out, "{END_STR}{keywords}{TAG_END}").expect("write!");
Ok(())
});
}
pub fn finish(self) {}
}
impl Drop for ItemEncoder<'_> {
fn drop(&mut self) {
self.doc.raw(&'\n');
}
}
pub trait EncodeOrd {
fn encode_cmp(&self, other: &Self) -> cmp::Ordering;
}
impl<T: Ord> EncodeOrd for T {
fn encode_cmp(&self, other: &Self) -> cmp::Ordering {
self.cmp(other)
}
}
pub trait NetdocEncodable {
fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
}
pub trait NetdocEncodableFields {
fn encode_fields(&self, out: &mut NetdocEncoder) -> Result<(), Bug>;
}
pub trait ItemValueEncodable {
fn write_item_value_onto(&self, out: ItemEncoder) -> Result<(), Bug>;
}
pub trait ItemObjectEncodable {
fn label(&self) -> &str;
fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug>;
}
pub trait NetdocBuilder {
fn build_sign<R: RngCore + CryptoRng>(self, rng: &mut R) -> Result<String, EncodeError>;
}
impl ItemValueEncodable for Void {
fn write_item_value_onto(&self, _out: ItemEncoder) -> Result<(), Bug> {
void::unreachable(*self)
}
}
impl ItemObjectEncodable for Void {
fn label(&self) -> &str {
void::unreachable(*self)
}
fn write_object_onto(&self, _: &mut Vec<u8>) -> Result<(), Bug> {
void::unreachable(*self)
}
}
macro_rules! item_value_encodable_for_tuple {
{ $($i:literal)* } => { paste! {
impl< $( [<T$i>]: ItemArgument, )* > ItemValueEncodable for ( $( [<T$i>], )* ) {
fn write_item_value_onto(
&self,
#[allow(unused)]
mut out: ItemEncoder,
) -> Result<(), Bug> {
$(
<[<T$i>] as ItemArgument>::write_arg_onto(&self.$i, &mut out)?;
)*
Ok(())
}
}
} }
}
item_value_encodable_for_tuple! {}
item_value_encodable_for_tuple! { 0 }
item_value_encodable_for_tuple! { 0 1 }
item_value_encodable_for_tuple! { 0 1 2 }
item_value_encodable_for_tuple! { 0 1 2 3 }
item_value_encodable_for_tuple! { 0 1 2 3 4 }
item_value_encodable_for_tuple! { 0 1 2 3 4 5 }
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 }
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 }
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 }
item_value_encodable_for_tuple! { 0 1 2 3 4 5 6 7 8 9 }
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use std::str::FromStr;
use crate::types::misc::Iso8601TimeNoSp;
use base64ct::{Base64Unpadded, Encoding};
#[test]
fn time_formats_as_args() {
use crate::doc::authcert::AuthCertKwd as ACK;
use crate::doc::netstatus::NetstatusKwd as NK;
let t_sp = Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap();
let t_no_sp = Iso8601TimeNoSp::from_str("2021-04-18T08:36:57").unwrap();
let mut encode = NetdocEncoder::new();
encode.item(ACK::DIR_KEY_EXPIRES).arg(&t_sp);
encode
.item(NK::SHARED_RAND_PREVIOUS_VALUE)
.arg(&"3")
.arg(&"bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs=")
.arg(&t_no_sp);
let doc = encode.finish().unwrap();
println!("{}", doc);
assert_eq!(
doc,
r"dir-key-expires 2020-04-18 08:36:57
shared-rand-previous-value 3 bMZR5Q6kBadzApPjd5dZ1tyLt1ckv1LfNCP/oyGhCXs= 2021-04-18T08:36:57
"
);
}
#[test]
fn authcert() {
use crate::doc::authcert::AuthCertKwd as ACK;
use crate::doc::authcert::{AuthCert, UncheckedAuthCert};
let pk_rsa = {
let pem = "
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE";
Base64Unpadded::decode_vec(&pem.replace('\n', "")).unwrap()
};
let mut encode = NetdocEncoder::new();
encode.item(ACK::DIR_KEY_CERTIFICATE_VERSION).arg(&3);
encode
.item(ACK::FINGERPRINT)
.arg(&"9367f9781da8eabbf96b691175f0e701b43c602e");
encode
.item(ACK::DIR_KEY_PUBLISHED)
.arg(&Iso8601TimeSp::from_str("2020-04-18 08:36:57").unwrap());
encode
.item(ACK::DIR_KEY_EXPIRES)
.arg(&Iso8601TimeSp::from_str("2021-04-18 08:36:57").unwrap());
encode
.item(ACK::DIR_IDENTITY_KEY)
.object("RSA PUBLIC KEY", &*pk_rsa);
encode
.item(ACK::DIR_SIGNING_KEY)
.object("RSA PUBLIC KEY", &*pk_rsa);
encode
.item(ACK::DIR_KEY_CROSSCERT)
.object("ID SIGNATURE", []);
encode
.item(ACK::DIR_KEY_CERTIFICATION)
.object("SIGNATURE", []);
let doc = encode.finish().unwrap();
eprintln!("{}", doc);
assert_eq!(
doc,
r"dir-key-certificate-version 3
fingerprint 9367f9781da8eabbf96b691175f0e701b43c602e
dir-key-published 2020-04-18 08:36:57
dir-key-expires 2021-04-18 08:36:57
dir-identity-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-signing-key
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBANUntsY9boHTnDKKlM4VfczcBE6xrYwhDJyeIkh7TPrebUBBvRBGmmV+
PYK8AM9irDtqmSR+VztUwQxH9dyEmwrM2gMeym9uXchWd/dt7En/JNL8srWIf7El
qiBHRBGbtkF/Re5pb438HC/CGyuujp43oZ3CUYosJOfY/X+sD0aVAgMBAAE=
-----END RSA PUBLIC KEY-----
dir-key-crosscert
-----BEGIN ID SIGNATURE-----
-----END ID SIGNATURE-----
dir-key-certification
-----BEGIN SIGNATURE-----
-----END SIGNATURE-----
"
);
let _: UncheckedAuthCert = AuthCert::parse(&doc).unwrap();
}
}