1use std::str::FromStr;
4use tor_error::Bug;
5
6use crate::encode::{NetdocEncodable, NetdocEncoder};
7use crate::parse2::{
8 ErrorProblem, IsStructural, ItemStream, KeywordRef, NetdocParseable, ParseInput,
9};
10
11use ErrorProblem as EP;
12
13use crate::parse2::poc::netstatus::vote::{
15 NetworkStatusUnverifiedParsedBody as NetworkStatusVoteUnverifiedParsedBody,
17};
18
19#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::AsRef)]
92pub struct EncodedAuthCert(#[as_ref(str)] String);
93
94#[derive(Copy, Clone, Debug, Eq, PartialEq)]
96enum ItemSequenceChecker {
97 Intro,
99 Body,
101 End,
103}
104
105struct IsOurStructural;
107
108const INTRO_KEYWORD: &str = "dir-key-certificate-version";
110const FINAL_KEYWORD: &str = "dir-key-certification";
112
113impl EncodedAuthCert {
114 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118}
119
120impl ItemSequenceChecker {
121 fn start() -> Self {
123 use ItemSequenceChecker::*;
124 Intro
125 }
126
127 fn keyword(&mut self, kw: KeywordRef<'_>) -> Result<Option<IsOurStructural>, EP> {
129 use ItemSequenceChecker::*;
130
131 let mut change_state = |from, to| {
132 if *self == from {
133 *self = to;
134 Ok(Some(IsOurStructural))
135 } else {
136 Err(EP::OtherBadDocument("authcert bad structure"))
137 }
138 };
139
140 if kw == INTRO_KEYWORD {
141 change_state(Intro, Body)
142 } else if kw == FINAL_KEYWORD {
143 change_state(Body, End)
144 } else if *self != Body {
145 Err(EP::OtherBadDocument(
146 "authcert loose body item or missing intro keyword",
147 ))
148 } else if let Some(IsStructural) =
149 NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(kw)
150 {
151 Err(EP::OtherBadDocument(
152 "authcert with vote structural keyword",
153 ))
154 } else if kw == "fingerprint" || kw.as_str().starts_with("dir-") {
155 Ok(None)
156 } else {
157 Err(EP::OtherBadDocument(
158 "authcert body keyword not dir- or fingerprint",
159 ))
160 }
161 }
162
163 fn finish(self) -> Result<(), EP> {
165 use ItemSequenceChecker::*;
166 match self {
167 End => Ok(()),
168 _other => Err(EP::OtherBadDocument(
169 "authcert missing end (signature) item",
170 )),
171 }
172 }
173}
174
175fn extra_lexical_checks(s: &str) -> Result<(), EP> {
180 let _without_trailing_nl = s
183 .strip_suffix("\n")
185 .ok_or(EP::OtherBadDocument("missing final newline"))?;
186
187 Ok(())
188}
189
190fn check(s: &str) -> Result<(), EP> {
192 extra_lexical_checks(s)?;
193
194 let input = ParseInput::new(s, "<authcert string>");
196 let mut lex = ItemStream::new(&input).map_err(|e| e.problem)?;
197 let mut seq = ItemSequenceChecker::start();
198 while let Some(item) = lex.next_item()? {
199 seq.keyword(item.keyword())?;
200 }
201 seq.finish()
202}
203
204impl TryFrom<String> for EncodedAuthCert {
205 type Error = ErrorProblem;
206 fn try_from(s: String) -> Result<Self, EP> {
207 check(&s)?;
208 Ok(EncodedAuthCert(s))
209 }
210}
211
212impl FromStr for EncodedAuthCert {
213 type Err = ErrorProblem;
214 fn from_str(s: &str) -> Result<Self, EP> {
215 s.to_owned().try_into()
216 }
217}
218
219impl NetdocParseable for EncodedAuthCert {
220 fn doctype_for_error() -> &'static str {
221 "encoded authority key certificate"
222 }
223
224 fn is_intro_item_keyword(kw: KeywordRef<'_>) -> bool {
225 kw == INTRO_KEYWORD
226 }
227 fn is_structural_keyword(kw: KeywordRef<'_>) -> Option<IsStructural> {
228 (Self::is_intro_item_keyword(kw) || kw == FINAL_KEYWORD).then_some(IsStructural)
229 }
230
231 fn from_items(input: &mut ItemStream<'_>, stop_at: stop_at!()) -> Result<Self, EP> {
232 let start_pos = input.byte_position();
233 let mut seq = ItemSequenceChecker::start();
234 while seq != ItemSequenceChecker::End {
235 let item = input.next_item()?.ok_or(EP::MissingItem {
236 keyword: FINAL_KEYWORD,
237 })?;
238
239 let kw = item.keyword();
240
241 match seq.keyword(kw)? {
242 Some(IsOurStructural) => {} None => {
244 if stop_at.stop_at(kw) {
245 return Err(EP::Internal(
246 "bug! parent document structural keyword found while trying to process an embedded authcert, but was accepted by ItemSequenceChecker; authcert embedded in something other than a vote?",
247 ));
248 }
249 }
250 }
251 }
252 seq.finish()?;
253
254 let text = input
255 .whole_input()
256 .get(start_pos..)
257 .expect("start_pos wasn't included in the body so far?!");
258
259 extra_lexical_checks(text)?;
260
261 if let Some(next_item) = input.peek_keyword()? {
262 if !stop_at.stop_at(next_item) {
263 return Err(EP::OtherBadDocument(
264 "unexpected loose items after embedded authcert",
265 ));
266 }
267 }
268
269 Ok(EncodedAuthCert(text.to_string()))
270 }
271}
272
273impl NetdocEncodable for EncodedAuthCert {
274 fn encode_unsigned(&self, out: &mut NetdocEncoder) -> Result<(), Bug> {
275 out.push_raw_string(&self.as_str());
277 Ok(())
278 }
279}
280
281#[cfg(test)]
282mod test {
283 #![allow(clippy::bool_assert_comparison)]
285 #![allow(clippy::clone_on_copy)]
286 #![allow(clippy::dbg_macro)]
287 #![allow(clippy::mixed_attributes_style)]
288 #![allow(clippy::print_stderr)]
289 #![allow(clippy::print_stdout)]
290 #![allow(clippy::single_char_pattern)]
291 #![allow(clippy::unwrap_used)]
292 #![allow(clippy::unchecked_time_subtraction)]
293 #![allow(clippy::useless_vec)]
294 #![allow(clippy::needless_pass_by_value)]
295 use super::*;
297 use crate::parse2::parse_netdoc;
298 use derive_deftly::Deftly;
299 use std::fmt::{Debug, Display};
300
301 #[derive(Debug, Deftly)]
302 #[derive_deftly(NetdocParseable)]
303 #[allow(unused)]
304 struct Embeds {
305 e_intro: (),
306 #[deftly(netdoc(subdoc))]
307 cert: EncodedAuthCert,
308 #[deftly(netdoc(subdoc))]
309 subdocs: Vec<Subdoc>,
310 }
311 #[derive(Debug, Deftly)]
312 #[derive_deftly(NetdocParseable)]
313 #[allow(unused)]
314 struct Subdoc {
315 dir_e_subdoc: (),
316 }
317
318 fn chk(exp_sole: Result<(), &str>, exp_embed: Result<(), &str>, doc: &str) {
319 fn chk1<T: Debug, E: Debug + tor_error::ErrorReport + Display>(
320 exp: Result<(), &str>,
321 doc: &str,
322 what: &str,
323 got: Result<T, E>,
324 ) {
325 eprintln!("==========\n---- {what} 8<- ----\n{doc}---- ->8 {what} ----\n");
326 match got {
327 Err(got_e) => {
328 let got_m = got_e.report().to_string();
329 eprintln!("{what}, got error: {got_e:?}");
330 eprintln!("{what}, got error: {got_m:?}");
331 let exp_m = exp.expect_err("expected success!");
332 assert!(
333 got_m.contains(exp_m),
334 "{what}, expected different error: {exp_m:?}"
335 );
336 }
337 y @ Ok(_) => {
338 eprintln!("got {y:?}");
339 assert!(exp.is_ok(), "{what}, unexpected success; expected: {exp:?}");
340 }
341 }
342 }
343 chk1(exp_sole, doc, "from_str", EncodedAuthCert::from_str(doc));
344 chk1(
345 exp_sole,
346 doc,
347 "From<String>",
348 EncodedAuthCert::try_from(doc.to_owned()),
349 );
350 let embeds = format!(
351 r"e-intro
352ignored
353{doc}dir-e-subdoc
354dir-ignored-2
355"
356 );
357 let parse_input = ParseInput::new(&embeds, "<embeds>");
358 chk1(
359 exp_embed,
360 &embeds,
361 "embedded",
362 parse_netdoc::<Embeds>(&parse_input),
363 );
364 }
365
366 #[test]
367 fn bad_authcerts() {
368 NetworkStatusVoteUnverifiedParsedBody::is_structural_keyword(
369 KeywordRef::new("dir-source").unwrap(),
370 )
371 .expect("structural dir-source");
372
373 chk(
378 Err("missing final newline"),
379 Err("missing item encoded authority key certificate"),
380 r"",
381 );
382 chk(
383 Err("authcert loose body item or missing intro keyword"),
384 Err("missing item encoded authority key certificate"),
385 r"wrong-intro
386",
387 );
388 chk(
389 Err("missing final newline"),
390 Err("missing item dir-key-certification"),
391 r"dir-key-certificate-version
392dir-missing-nl",
393 );
394 chk(
395 Err("authcert bad structure"),
396 Err("authcert bad structure"),
397 r"dir-key-certificate-version
398dir-key-certificate-version
399",
400 );
401 chk(
402 Err("authcert body keyword not dir- or fingerprint"),
403 Err("authcert body keyword not dir- or fingerprint"),
404 r"dir-key-certificate-version
405wrong-item
406dir-key-certification
407",
408 );
409 chk(
410 Err("authcert with vote structural keyword"),
411 Err("authcert with vote structural keyword"),
412 r"dir-key-certificate-version
413r
414dir-key-certification
415",
416 );
417 chk(
418 Err("authcert with vote structural keyword"),
419 Err("authcert with vote structural keyword"),
420 r"dir-key-certificate-version
421dir-source
422dir-key-certification
423",
424 );
425 chk(
426 Ok(()), Err("bug! parent document structural keyword found"),
428 r"dir-key-certificate-version
429dir-e-subdoc
430dir-key-certification
431",
432 );
433 chk(
434 Err("authcert with vote structural keyword"),
435 Err("authcert with vote structural keyword"),
436 r"dir-key-certificate-version
437dir-example-item
438r
439",
440 );
441 chk(
442 Err("authcert loose body item or missing intro keyword"),
443 Err("unexpected loose items after embedded authcert"),
444 r"dir-key-certificate-version
445dir-example-item
446dir-key-certification
447dir-extra-item
448r
449",
450 );
451 chk(
452 Err("authcert bad structure"),
453 Err("authcert bad structure"),
454 r"dir-key-certificate-version
455dir-key-certificate-version
456dir-example-item
457dir-key-certification
458dir-key-certification
459r
460",
461 );
462 chk(
463 Err("authcert bad structure"),
464 Err("unexpected loose items after embedded authcert"),
465 r"dir-key-certificate-version
466dir-example-item
467dir-key-certification
468dir-key-certification
469r
470",
471 );
472 }
473}