1
// @@ begin test lint list maintained by maint/add_warning @@
2
#![allow(clippy::bool_assert_comparison)]
3
#![allow(clippy::clone_on_copy)]
4
#![allow(clippy::dbg_macro)]
5
#![allow(clippy::mixed_attributes_style)]
6
#![allow(clippy::print_stderr)]
7
#![allow(clippy::print_stdout)]
8
#![allow(clippy::single_char_pattern)]
9
#![allow(clippy::unwrap_used)]
10
#![allow(clippy::unchecked_time_subtraction)]
11
#![allow(clippy::useless_vec)]
12
#![allow(clippy::needless_pass_by_value)]
13
#![allow(clippy::string_slice)] // See arti#2571
14
//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
15
#![allow(clippy::needless_borrows_for_generic_args)] // TODO add to maint/add_warning
16

            
17
use std::fmt::{self, Debug};
18
use std::mem;
19
use std::slice;
20

            
21
use anyhow::Context as _;
22
use derive_deftly::Deftly;
23
use testresult::TestResult;
24
use tor_error::{Bug, ErrorReport as _};
25

            
26
use crate::encode::{ItemEncoder, ItemObjectEncodable, NetdocEncodable, NetdocEncoder};
27
use crate::parse2::{
28
    ArgumentError as P2AE, ArgumentStream, ErrorProblem as P2EP, ItemObjectParseable,
29
    NetdocParseable, ParseError, ParseInput, UnparsedItem, parse_netdoc, parse_netdoc_multiple,
30
    parse_netdoc_multiple_with_offsets,
31
};
32
use crate::types::{Ignored, NotPresent};
33

            
34
34
fn default<T: Default>() -> T {
35
34
    Default::default()
36
34
}
37

            
38
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
39
#[derive_deftly(NetdocEncodable, NetdocParseable)]
40
struct Top {
41
    top_intro: (),
42
    needed: (String,),
43
    optional: Option<(String,)>,
44
    several: Vec<(String,)>,
45
    not_present: NotPresent,
46
    #[deftly(netdoc(default))]
47
    defaulted: (i32,),
48
    #[deftly(netdoc(keyword = "renamed"))]
49
    t4_renamed: Option<(String,)>,
50
    #[deftly(netdoc(subdoc))]
51
    sub1: Sub1,
52
    #[deftly(netdoc(subdoc))]
53
    sub2: Option<Sub2>,
54
    #[deftly(netdoc(subdoc))]
55
    sub3: Vec<Sub3>,
56
    #[deftly(netdoc(subdoc, default))]
57
    sub4: Sub4,
58
}
59

            
60
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
61
#[derive_deftly(NetdocEncodable, NetdocParseable)]
62
struct Sub1 {
63
    sub1_intro: (),
64
    sub1_field: Option<(String,)>,
65
    #[deftly(netdoc(flatten))]
66
    flatten: Flat1,
67
}
68
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
69
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
70
struct Flat1 {
71
    flat_needed: (String,),
72
    flat_optional: Option<(String,)>,
73
    flat_several: Vec<(String,)>,
74
    flat_defaulted: Option<(String,)>,
75
    #[deftly(netdoc(single_arg))]
76
    flat_arg_needed: String,
77
    #[deftly(netdoc(single_arg))]
78
    flat_arg_optional: Option<String>,
79
    #[deftly(netdoc(single_arg))]
80
    flat_arg_several: Vec<String>,
81
    #[deftly(netdoc(single_arg, default))]
82
    flat_arg_defaulted: i32,
83
    #[deftly(netdoc(with = needs_with_parse))]
84
    flat_with_needed: NeedsWith,
85
    #[deftly(netdoc(with = needs_with_parse))]
86
    flat_with_optional: Option<NeedsWith>,
87
    #[deftly(netdoc(with = needs_with_parse))]
88
    flat_with_several: Vec<NeedsWith>,
89
    #[deftly(netdoc(flatten))]
90
    flat_flat: FlatInner,
91
}
92
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
93
#[derive_deftly(NetdocEncodableFields, NetdocParseableFields)]
94
struct FlatInner {
95
    flat_inner_optional: Option<(String,)>,
96
}
97
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
98
#[derive_deftly(NetdocEncodable, NetdocParseable)]
99
struct Sub2 {
100
    #[deftly(netdoc(with = needs_with_intro))]
101
    sub2_intro: NeedsWith,
102
    sub2_field: Option<(String,)>,
103
    #[deftly(netdoc(single_arg))]
104
    arg_needed: String,
105
    #[deftly(netdoc(single_arg))]
106
    arg_optional: Option<String>,
107
    #[deftly(netdoc(single_arg))]
108
    arg_several: Vec<String>,
109
    #[deftly(netdoc(single_arg, default))]
110
    arg_defaulted: i32,
111
    #[deftly(netdoc(with = needs_with_parse))]
112
    with_needed: NeedsWith,
113
    #[deftly(netdoc(with = needs_with_parse))]
114
    with_optional: Option<NeedsWith>,
115
    #[deftly(netdoc(with = "needs_with_parse"))] // leave one with = "..."
116
    with_several: Vec<NeedsWith>,
117
    #[deftly(netdoc(subdoc))]
118
    subsub: SubSub,
119
}
120
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
121
#[derive_deftly(NetdocEncodable, NetdocParseable)]
122
struct Sub3 {
123
    sub3_intro: (),
124
    sub3_field: Option<(String,)>,
125
}
126
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
127
#[derive_deftly(NetdocEncodable, NetdocParseable)]
128
struct Sub4 {
129
    sub4_intro: (),
130
    sub4_field: Option<(String,)>,
131
}
132
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
133
#[derive_deftly(NetdocEncodable, NetdocParseable)]
134
struct SubSub {
135
    #[deftly(netdoc(single_arg))]
136
    subsub_intro: String,
137
    subsub_field: Option<(String,)>,
138
}
139

            
140
#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)]
141
struct NeedsWith;
142

            
143
impl NeedsWith {
144
112
    fn parse_expecting(exp: &str, args: &mut ArgumentStream<'_>) -> Result<NeedsWith, P2AE> {
145
112
        let got = args.next().ok_or(P2AE::Missing)?;
146
110
        (got == exp).then_some(NeedsWith).ok_or(P2AE::Invalid)
147
112
    }
148
}
149

            
150
mod needs_with_parse {
151
    use super::*;
152
82
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
153
82
        NeedsWith::parse_expecting("normal", item.args_mut())
154
82
            .map_err(item.args().error_handler("in needs with"))
155
82
    }
156
    #[allow(clippy::unnecessary_wraps)]
157
24
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
158
24
        out.arg(&"normal");
159
24
        Ok(())
160
24
    }
161
}
162
mod needs_with_intro {
163
    use super::*;
164
18
    pub(super) fn from_unparsed(mut item: UnparsedItem<'_>) -> Result<NeedsWith, P2EP> {
165
18
        NeedsWith::parse_expecting("intro", item.args_mut())
166
18
            .map_err(item.args().error_handler("in needs with"))
167
18
    }
168
    #[allow(clippy::unnecessary_wraps)]
169
4
    pub(super) fn write_item_value_onto(_: &NeedsWith, out: ItemEncoder) -> Result<(), Bug> {
170
4
        out.arg(&"intro");
171
4
        Ok(())
172
4
    }
173
}
174
mod needs_with_arg {
175
    use super::*;
176
12
    pub(super) fn from_args(args: &mut ArgumentStream) -> Result<NeedsWith, P2AE> {
177
12
        NeedsWith::parse_expecting("arg", args)
178
12
    }
179
    #[allow(clippy::unnecessary_wraps)]
180
4
    pub(super) fn write_arg_onto(_self: &NeedsWith, out: &mut ItemEncoder<'_>) -> Result<(), Bug> {
181
4
        out.args_raw_string(&"arg");
182
4
        Ok(())
183
4
    }
184
6
    pub(super) fn from_args_rest(s: &str) -> Result<NeedsWith, ()> {
185
6
        (s == "rest of line").then_some(NeedsWith).ok_or(())
186
6
    }
187
    #[allow(clippy::unnecessary_wraps)]
188
2
    pub(super) fn fmt_args_rest(_self: &NeedsWith, f: &mut fmt::Formatter) -> fmt::Result {
189
2
        write!(f, "rest of line")
190
2
    }
191
}
192

            
193
/// Test parsing and encoding of a single-document file
194
///
195
/// `doc_spec` is the document to parse.
196
/// `exp` is what it should parse as.
197
///
198
/// `doc_spec` can have magic instructions at end of each line.
199
/// These allow the re-encoding to be not quite identical to the input document.
200
///
201
///  * **`@ re-encoded:`:
202
///    This line is re-encoded differently.  The *next* source line is the encoding.
203
///
204
///  * **`@ not re-encoded`:
205
///    This line is omitted from the re-encoding.
206
///
207
///  * **`@ re-encoded later N`:
208
///    This line is reordered, to later in the re-encoding, by `N` lines.
209
///
210
///  * **`@ re-encoded only`:
211
///    This line eppears only in the re-encoding.
212
///    Prefer `re-encoded later` or `re-encoded:` if possible as they're clearer.
213
14
fn t_ok<D>(doc_spec: &str, exp: &D) -> TestResult<()>
214
14
where
215
14
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
216
{
217
14
    t_ok_multi::<D>(&[], doc_spec, slice::from_ref(exp))
218
14
}
219

            
220
/// Test parsing and encoding of a multi-document file
221
///
222
/// The de/re-encoding syntax is as above.
223
//
224
// It would perhaps be better if `doc_boundaries` were obtained from magic instructions,
225
// but there's only one test case with a fragile hardcoded byte offset ATM.
226
16
fn t_ok_multi<D>(doc_boundaries: &[usize], doc_spec: &str, exp: &[D]) -> TestResult<()>
227
16
where
228
16
    D: NetdocEncodable + NetdocParseable + Debug + PartialEq,
229
{
230
16
    eprintln!("#####");
231
16
    eprint!("====== doc_spec ======\n{doc_spec}");
232
16
    eprintln!("====== exp ======\n{exp:#?}");
233

            
234
16
    let mut lines = doc_spec.split_inclusive('\n');
235
16
    let mut doc = String::new();
236
16
    let mut enc = String::new();
237

            
238
    // indices are line numbers but starting at 0
239
16
    let mut moved = Vec::<String>::new();
240
216
    let process_moved = |enc: &mut String, moved: &mut Vec<String>| {
241
216
        if moved.is_empty() {
242
126
            return;
243
90
        }
244
        loop {
245
112
            let lno = enc.lines().count();
246
112
            let Some(m) = moved.get_mut(lno) else {
247
36
                eprintln!("PN {lno:2} nothing");
248
36
                break;
249
            };
250
76
            if m.is_empty() {
251
54
                eprintln!("PN {lno:2} empty");
252
54
                break;
253
22
            }
254
22
            eprintln!("PN {lno:2} adding {m:?}");
255
22
            *enc += &mem::take(m);
256
        }
257
216
    };
258

            
259
216
    while let Some(l) = lines.next() {
260
200
        if let Some((l, insn)) = l.split_once('@') {
261
56
            eprintln!("LL    insn  {l:?}");
262
56
            let insn = insn.trim();
263
56
            let l = &format!("{}\n", l.trim_end());
264
56
            let insn = insn.trim_end();
265
56
            if insn == "re-encoded:" {
266
6
                doc += l;
267
6
                enc += lines.next().expect(r#""re-encoded:" needs re-encoded"#);
268
50
            } else if insn == "not re-encoded" {
269
10
                doc += l;
270
40
            } else if insn == "re-encoded only" {
271
12
                enc += l;
272
28
            } else if let Some(later) = insn.strip_prefix("re-encoded later ") {
273
28
                doc += l;
274
28
                let later: usize = later.parse().expect(later);
275
28
                let lno = later + enc.lines().count();
276
                loop {
277
120
                    if let Some(m) = moved.get_mut(lno) {
278
28
                        *m += l;
279
28
                        break;
280
92
                    }
281
92
                    moved.push("".into());
282
                }
283
            } else {
284
                panic!("unknown insn {insn:?} in {doc_spec:?}");
285
            }
286
144
        } else {
287
144
            eprintln!("LL    line  {l:?}");
288
144
            doc += l;
289
144
            enc += l;
290
144
        }
291
200
        process_moved(&mut enc, &mut moved);
292
    }
293
16
    process_moved(&mut enc, &mut moved);
294
92
    for (i, l) in moved.iter().enumerate() {
295
92
        assert_eq!(l, "", "line too late! {}: {l:?}", i + 1);
296
    }
297

            
298
16
    eprint!("====== doc ======\n{doc}");
299
16
    eprint!("====== enc exp ======\n{enc}");
300
16
    eprintln!("======");
301

            
302
16
    let input = ParseInput::new(&doc, "<literal>");
303

            
304
16
    if exp.len() == 1 {
305
14
        let got = parse_netdoc::<D>(&input).context(doc.clone())?;
306
14
        assert_eq!(got, exp[0], "parse 1 mismatch");
307
2
    }
308

            
309
16
    let got = parse_netdoc_multiple::<D>(&input)?;
310
16
    assert_eq!(got, exp, "parse_multiple mismatch");
311

            
312
16
    let got_with_offsets = parse_netdoc_multiple_with_offsets::<D>(&input)?;
313
18
    for (i, (got, start, end)) in got_with_offsets.iter().enumerate() {
314
18
        assert_eq!(got, &exp[i], "parse_multiple_with_offsets mismatch");
315
18
        assert_eq!(*start, if i == 0 { 0 } else { doc_boundaries[i - 1] });
316
18
        assert_eq!(*end, doc_boundaries.get(i).copied().unwrap_or(doc.len()));
317
    }
318

            
319
16
    let reenc = {
320
16
        let mut encoder = NetdocEncoder::default();
321
18
        for d in exp {
322
18
            d.encode_unsigned(&mut encoder)?;
323
        }
324
16
        encoder.finish()?
325
    };
326

            
327
16
    eprintln!("====== enc got ======\n{reenc}====== end ======");
328

            
329
16
    assert_eq_or_diff!(&enc, &reenc,);
330

            
331
16
    Ok(())
332
16
}
333

            
334
#[allow(clippy::unnecessary_wraps)] // Result for consistency
335
38
fn t_err_raw<D>(
336
38
    exp_lno: usize,
337
38
    exp_col: Option<usize>,
338
38
    exp_err: &str,
339
38
    doc: &str,
340
38
) -> TestResult<ParseError>
341
38
where
342
38
    D: NetdocParseable + Debug,
343
{
344
38
    let input = ParseInput::new(doc, "<massaged>");
345
38
    let got = parse_netdoc::<D>(&input).expect_err("unexpectedly parsed ok");
346
38
    let got_err = got.problem.to_string();
347
38
    assert_eq!(
348
38
        (got.lno, got.column),
349
38
        (exp_lno, exp_col),
350
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
351
        got_err
352
    );
353
38
    assert_eq!(
354
        got_err, exp_err,
355
        "doc\n====\n{doc}====\n got={}\n exp={exp_err}",
356
        got_err
357
    );
358
38
    Ok(got)
359
38
}
360

            
361
/// Test an error case with embedded error message
362
///
363
/// `case` should be the input document, but exactly one line should
364
/// contain `" # "`, with the expected error message as a "comment".
365
///
366
/// Iff the expected message is supposed to have a column number,
367
/// the comment part should end with ` @<column>`.
368
///
369
/// `t_err` will check that that error is reported, at that line.
370
36
fn t_err<D>(mut case: &str) -> TestResult<ParseError>
371
36
where
372
36
    D: NetdocParseable + Debug,
373
{
374
36
    let mut exp = None;
375
36
    let mut doc = String::new();
376
36
    let mut lno = 0;
377
192
    while let Some((l, r)) = case.split_once('\n') {
378
156
        lno += 1;
379
156
        case = r;
380
156
        if let Some((l, r)) = l.split_once(" # ") {
381
36
            assert!(exp.is_none());
382
36
            exp = Some((lno, r.trim()));
383
36
            let l = l.trim_end();
384
36
            doc += l;
385
120
        } else {
386
120
            doc += l;
387
120
        }
388
156
        doc += "\n";
389
    }
390
36
    if !case.is_empty() {
391
        panic!("missing final newline");
392
36
    }
393
36
    let (exp_lno, exp_err) = exp.expect("missing # error indication in test case");
394
36
    let (exp_err, exp_col) = if let Some((l, r)) = exp_err.rsplit_once(" @") {
395
4
        (l, Some(r.parse().unwrap()))
396
    } else {
397
32
        (exp_err, None)
398
    };
399
36
    println!("==== 8<- ====\n{doc}==== ->8 ====");
400
36
    t_err_raw::<D>(exp_lno, exp_col, exp_err, &doc)
401
36
}
402

            
403
/// Test an error case with embedded error message
404
///
405
/// `case` should be the input document, but exactly one line should
406
/// contain `" # "`, with the expected error message as a "comment".
407
///
408
/// Iff the expected message is supposed to have a column number,
409
/// the comment part should end with ` @<column>`.
410
///
411
/// `t_err` will check that that error is reported, at that column.
412
4
fn t_err_chk_msg<D>(case: &str, msg: &str) -> TestResult
413
4
where
414
4
    D: NetdocParseable + Debug,
415
{
416
4
    let err = t_err::<D>(case)?;
417
4
    assert_eq!(err.report().to_string(), msg);
418
4
    Ok(())
419
4
}
420

            
421
#[test]
422
2
fn various_docs() -> TestResult<()> {
423
41
    let val = |s: &str| (s.to_owned(),);
424
23
    let sval = |s: &str| Some(val(s));
425

            
426
2
    let sub1_minimal = Sub1 {
427
2
        flatten: Flat1 {
428
2
            flat_needed: val("FN"),
429
2
            flat_arg_needed: "FAN".into(),
430
2
            ..default()
431
2
        },
432
2
        ..default()
433
2
    };
434
2
    let subsub_minimal = SubSub {
435
2
        subsub_intro: "SSI".into(),
436
2
        ..default()
437
2
    };
438
2
    let sub2_minimal = Sub2 {
439
2
        arg_needed: "AN".into(),
440
2
        subsub: subsub_minimal.clone(),
441
2
        ..default()
442
2
    };
443

            
444
2
    t_ok(
445
2
        r#"top-intro
446
2
needed N
447
2
defaulted 0                             @ re-encoded only
448
2
sub1-intro
449
2
flat-needed FN
450
2
flat-arg-needed FAN
451
2
flat-arg-defaulted 0                    @ re-encoded only
452
2
flat-with-needed normal
453
2
sub4-intro                              @ re-encoded only
454
2
"#,
455
2
        &Top {
456
2
            needed: val("N"),
457
2
            sub1: sub1_minimal.clone(),
458
2
            ..default()
459
2
        },
460
    )?;
461

            
462
2
    t_ok(
463
2
        r#"top-intro
464
2
needed N
465
2
defaulted 0                             @ re-encoded only
466
2
sub1-intro
467
2
flat-needed FN
468
2
flat-arg-needed FAN
469
2
flat-arg-defaulted 0                    @ re-encoded only
470
2
flat-with-needed normal
471
2
sub2-intro intro
472
2
with-needed normal                      @ re-encoded later 2
473
2
arg-needed AN
474
2
arg-defaulted 0                         @ re-encoded only
475
2
subsub-intro SSI
476
2
sub3-intro
477
2
sub3-intro
478
2
sub4-intro
479
2
"#,
480
2
        &Top {
481
2
            needed: val("N"),
482
2
            sub1: sub1_minimal.clone(),
483
2
            sub2: Some(sub2_minimal.clone()),
484
2
            sub3: vec![default(); 2],
485
2
            ..default()
486
2
        },
487
    )?;
488

            
489
2
    t_ok(
490
2
        r#"top-intro
491
2
needed N
492
2
optional O
493
2
several 1
494
2
not-present oh yes it is                @ not re-encoded
495
2
not-present but it is ignored           @ not re-encoded
496
2
several 2
497
2
defaulted -1
498
2
renamed R
499
2
sub1-intro
500
2
flat-several FS1                        @ re-encoded later 3
501
2
flat-needed FN                          @ re-encoded later 1
502
2
flat-with-needed normal                 @ re-encoded later 11
503
2
flat-inner-optional nested              @ re-encoded later 15
504
2
sub1-field A
505
2
flat-with-several normal                @ re-encoded later 11
506
2
flat-with-several normal                @ re-encoded later 11
507
2
flat-optional FO
508
2
flat-arg-needed FAN                     @ re-encoded later 2
509
2
flat-with-optional normal               @ re-encoded later 8
510
2
flat-several FS2
511
2
flat-defaulted FD
512
2
flat-arg-optional FAO
513
2
flat-arg-several FAS1 ignored           @ re-encoded:
514
2
flat-arg-several FAS1
515
2
flat-arg-several FAS2
516
2
flat-arg-defaulted 31
517
2
sub2-intro intro
518
2
with-several normal                     @ re-encoded later 8
519
2
with-several normal                     @ re-encoded later 8
520
2
with-several normal                     @ re-encoded later 8
521
2
sub2-field B
522
2
arg-needed AN
523
2
arg-optional AO
524
2
with-optional normal                    @ re-encoded later 4
525
2
arg-defaulted 4                         @ re-encoded later 2
526
2
arg-several A1
527
2
arg-several A2
528
2
with-needed normal
529
2
subsub-intro SSI
530
2
subsub-field BS
531
2
sub3-intro
532
2
sub3-field C1
533
2
sub3-intro
534
2
sub3-field C2
535
2
sub4-intro
536
2
sub4-field D
537
2
"#,
538
        &Top {
539
2
            needed: val("N"),
540
2
            optional: sval("O"),
541
2
            several: ["1", "2"].map(val).into(),
542
2
            defaulted: (-1,),
543
2
            t4_renamed: sval("R"),
544
2
            sub1: Sub1 {
545
2
                sub1_field: sval("A"),
546
2
                flatten: Flat1 {
547
2
                    flat_needed: val("FN"),
548
2
                    flat_optional: sval("FO"),
549
2
                    flat_several: ["FS1", "FS2"].map(val).into(),
550
2
                    flat_defaulted: sval("FD"),
551
2
                    flat_arg_needed: "FAN".into(),
552
2
                    flat_arg_several: ["FAS1", "FAS2"].map(Into::into).into(),
553
2
                    flat_arg_optional: Some("FAO".into()),
554
2
                    flat_arg_defaulted: 31,
555
2
                    flat_with_optional: Some(NeedsWith),
556
2
                    flat_with_several: vec![NeedsWith; 2],
557
2
                    flat_flat: FlatInner {
558
2
                        flat_inner_optional: sval("nested"),
559
2
                    },
560
2
                    ..Flat1::default()
561
2
                },
562
2
                ..default()
563
2
            },
564
2
            sub2: Some(Sub2 {
565
2
                sub2_field: sval("B"),
566
2
                arg_optional: Some("AO".into()),
567
2
                arg_defaulted: 4,
568
2
                arg_several: ["A1", "A2"].map(Into::into).into(),
569
2
                with_optional: Some(NeedsWith),
570
2
                with_several: vec![NeedsWith; 3],
571
2
                subsub: SubSub {
572
2
                    subsub_field: sval("BS"),
573
2
                    ..subsub_minimal.clone()
574
2
                },
575
2
                ..sub2_minimal.clone()
576
2
            }),
577
2
            sub3: ["C1", "C2"]
578
2
                .map(|s| Sub3 {
579
4
                    sub3_field: sval(s),
580
4
                    ..default()
581
4
                })
582
2
                .into(),
583
2
            sub4: Sub4 {
584
2
                sub4_field: sval("D"),
585
2
                ..default()
586
2
            },
587
2
            ..default()
588
        },
589
    )?;
590

            
591
2
    t_err_raw::<Top>(0, None, "empty document", r#""#)?;
592

            
593
2
    let wrong_document = r#"wrong-keyword # wrong document type
594
2
"#;
595
2
    t_err_chk_msg::<Top>(
596
2
        wrong_document,
597
2
        "error: failed to parse network document, type top-intro: <massaged>:1: wrong document type",
598
    )?;
599

            
600
2
    t_err::<Top>(
601
2
        r#"top-intro
602
2
sub4-intro # missing item needed
603
2
"#,
604
    )?;
605

            
606
2
    t_err::<Top>(
607
2
        r#"top-intro
608
2
sub1-intro
609
2
flat-arg-needed arg
610
2
flat-with-needed normal
611
2
sub4-intro # missing item flat-needed
612
2
"#,
613
    )?;
614

            
615
2
    t_err::<Top>(
616
2
        r#"top-intro
617
2
sub1-intro
618
2
flat-needed flat
619
2
flat-with-needed normal
620
2
sub4-intro # missing item flat-arg-needed
621
2
"#,
622
    )?;
623

            
624
2
    t_err::<Top>(
625
2
        r#"top-intro
626
2
sub1-intro
627
2
flat-arg-needed FAN
628
2
flat-with-needed normal
629
2
flat-needed FN
630
2
sub4-intro # missing item needed
631
2
"#,
632
    )?;
633

            
634
2
    t_err::<Top>(
635
2
        r#"top-intro
636
2
needed N
637
2
sub3-intro
638
2
sub4-intro # missing item sub1-intro
639
2
"#,
640
    )?;
641

            
642
2
    t_err::<Top>(
643
2
        r#"top-intro
644
2
needed N
645
2
sub1-intro
646
2
flat-needed FN1
647
2
flat-arg-needed FAN
648
2
flat-with-needed normal
649
2
sub1-intro # item repeated when not allowed
650
2
flat-needed FN2
651
2
"#,
652
    )?;
653

            
654
2
    t_err::<Top>(
655
2
        r#"top-intro
656
2
needed N
657
2
sub2-intro # missing argument in needs with
658
2
"#,
659
    )?;
660

            
661
2
    let wrong_value = r#"top-intro
662
2
needed N
663
2
sub2-intro wrong-value # invalid value for argument in needs with @12
664
2
"#;
665
2
    t_err_chk_msg::<Top>(
666
2
        wrong_value,
667
2
        "error: failed to parse network document, type top-intro: <massaged>:3.12: invalid value for argument in needs with",
668
    )?;
669

            
670
2
    t_err::<Top>(
671
2
        r#"top-intro
672
2
sub1-intro
673
2
flat-needed FN
674
2
flat-arg-needed arg
675
2
sub4-intro # missing item flat-with-needed
676
2
"#,
677
    )?;
678

            
679
2
    t_err::<Top>(
680
2
        r#"top-intro
681
2
sub1-intro
682
2
flat-needed FN
683
2
flat-arg-needed arg
684
2
flat-with-needed normal
685
2
sub2-intro intro
686
2
arg-needed AN
687
2
flat-arg-needed arg
688
2
sub3-intro # missing item with-needed
689
2
"#,
690
    )?;
691

            
692
2
    Ok(())
693
2
}
694

            
695
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
696
#[derive_deftly(NetdocEncodable, NetdocParseable)]
697
struct TopMinimal {
698
    test_item0: TestItem0,
699
    test_item: Option<TestItem>,
700
    test_item_rest: Option<TestItemRest>,
701
    test_item_rest_with: Option<TestItemRestWith>,
702
    test_item_object_not_present: Option<TestItemObjectNotPresent>,
703
    test_item_object_ignored: Option<TestItemObjectIgnored>,
704
    #[deftly(netdoc(skip))]
705
    __test_skip: (),
706
}
707

            
708
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
709
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
710
#[deftly(netdoc(no_extra_args))]
711
struct TestItem0 {
712
    #[deftly(netdoc(object(label = "UTF-8 STRING"), with = string_data_object))]
713
    object: Option<String>,
714
}
715

            
716
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
717
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
718
#[deftly(netdoc(debug))] // For testing our debugging arrangements
719
struct TestItem {
720
    #[deftly(netdoc(skip))]
721
    skip: u32,
722
    needed: String,
723
    #[deftly(netdoc(with = needs_with_arg))]
724
    optional: Option<NeedsWith>,
725
    rest: Vec<String>,
726
    #[deftly(netdoc(object))]
727
    object: TestObject,
728
}
729

            
730
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
731
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
732
struct TestItemRest {
733
    optional: Option<String>,
734
    #[deftly(netdoc(rest))]
735
    rest: String,
736
}
737

            
738
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
739
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
740
struct TestItemRestWith {
741
    #[deftly(netdoc(rest, with = needs_with_arg))]
742
    rest: NeedsWith,
743
}
744

            
745
#[derive(Debug, Default, Clone, Eq, PartialEq)]
746
struct TestObject(String);
747

            
748
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
749
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
750
struct TestItemObjectNotPresent {
751
    #[deftly(netdoc(object))]
752
    object: NotPresent,
753
}
754

            
755
#[derive(Deftly, Debug, Default, Clone, Eq, PartialEq)]
756
#[derive_deftly(ItemValueEncodable, ItemValueParseable)]
757
struct TestItemObjectIgnored {
758
    #[deftly(netdoc(object))]
759
    object: Ignored,
760
}
761

            
762
/// Conversion module for `String` as Object with [`ItemValueParseable`]
763
mod string_data_object {
764
    /// Parse the data
765
8
    pub(super) fn try_from(data: Vec<u8>) -> Result<String, std::string::FromUtf8Error> {
766
8
        String::from_utf8(data)
767
8
    }
768

            
769
    /// Encode the data
770
    #[allow(clippy::unnecessary_wraps)] // signature must match the derive's expectation
771
2
    pub(super) fn write_object_onto<B>(self_: &String, b: &mut B) -> tor_bytes::EncodeResult<()>
772
2
    where
773
2
        B: tor_bytes::Writer + ?Sized,
774
    {
775
2
        b.write_all(self_.as_bytes());
776
2
        Ok(())
777
2
    }
778
}
779

            
780
impl ItemObjectParseable for TestObject {
781
18
    fn check_label(label: &str) -> Result<(), P2EP> {
782
18
        if label != "TEST OBJECT" {
783
            return Err(P2EP::ObjectIncorrectLabel);
784
18
        }
785
18
        Ok(())
786
18
    }
787
18
    fn from_bytes(data: &[u8]) -> Result<Self, P2EP> {
788
        Ok(TestObject(
789
18
            String::from_utf8(data.to_owned()).map_err(|_| P2EP::ObjectInvalidData)?,
790
        ))
791
18
    }
792
}
793
impl ItemObjectEncodable for TestObject {
794
6
    fn label(&self) -> &'static str {
795
6
        "TEST OBJECT"
796
6
    }
797
6
    fn write_object_onto(&self, b: &mut Vec<u8>) -> Result<(), Bug> {
798
6
        b.extend(self.0.as_bytes());
799
6
        Ok(())
800
6
    }
801
}
802

            
803
#[test]
804
2
fn various_items() -> TestResult<()> {
805
2
    let test_item_minimal = TestItem {
806
2
        needed: "N".into(),
807
2
        object: TestObject("hello".into()),
808
2
        ..default()
809
2
    };
810

            
811
2
    t_ok(
812
2
        r#"test-item0
813
2
"#,
814
2
        &TopMinimal { ..default() },
815
    )?;
816

            
817
2
    t_ok(
818
2
        r#"test-item0
819
2
test-item N
820
2
-----BEGIN TEST OBJECT-----
821
2
aGVsbG8=
822
2
-----END TEST OBJECT-----
823
2
"#,
824
2
        &TopMinimal {
825
2
            test_item: Some(test_item_minimal.clone()),
826
2
            ..default()
827
2
        },
828
    )?;
829

            
830
2
    t_ok(
831
2
        r#"test-item0
832
2
test-item N arg
833
2
-----BEGIN TEST OBJECT-----
834
2
aGVsbG8=
835
2
-----END TEST OBJECT-----
836
2
"#,
837
2
        &TopMinimal {
838
2
            test_item: Some(TestItem {
839
2
                optional: Some(NeedsWith),
840
2
                ..test_item_minimal.clone()
841
2
            }),
842
2
            ..default()
843
2
        },
844
    )?;
845

            
846
2
    t_ok(
847
2
        r#"test-item0
848
2
-----BEGIN UTF-8 STRING-----
849
2
aGVsbG8=
850
2
-----END UTF-8 STRING-----
851
2
test-item N arg R1 R2
852
2
-----BEGIN TEST OBJECT-----
853
2
aGVsbG8=
854
2
-----END TEST OBJECT-----
855
2
test-item-rest O  and  the rest                 @ re-encoded:
856
2
test-item-rest O and  the rest
857
2
test-item-rest-with   rest of line              @ re-encoded:
858
2
test-item-rest-with rest of line
859
2
test-item-object-not-present
860
2
test-item-object-ignored
861
2
-----BEGIN TEST OBJECT-----                     @ not re-encoded
862
2
aGVsbG8=         @ not re-encoded
863
2
-----END TEST OBJECT-----                       @ not re-encoded
864
2
"#,
865
2
        &TopMinimal {
866
2
            test_item0: TestItem0 {
867
2
                object: Some("hello".into()),
868
2
            },
869
2
            test_item: Some(TestItem {
870
2
                optional: Some(NeedsWith),
871
2
                rest: ["R1", "R2"].map(Into::into).into(),
872
2
                ..test_item_minimal.clone()
873
2
            }),
874
2
            test_item_rest: Some(TestItemRest {
875
2
                optional: Some("O".into()),
876
2
                rest: "and  the rest".into(),
877
2
            }),
878
2
            test_item_rest_with: Some(TestItemRestWith { rest: NeedsWith }),
879
2
            test_item_object_not_present: Some(TestItemObjectNotPresent { object: NotPresent }),
880
2
            test_item_object_ignored: Some(TestItemObjectIgnored { object: Ignored }),
881
2
            __test_skip: (),
882
2
        },
883
    )?;
884

            
885
2
    t_ok_multi(
886
2
        &[11],
887
2
        r#"test-item0
888
2
test-item0
889
2
test-item-rest optional resty rest
890
2
"#,
891
2
        &[
892
2
            TopMinimal::default(),
893
2
            TopMinimal {
894
2
                test_item_rest: Some(TestItemRest {
895
2
                    optional: Some("optional".into()),
896
2
                    rest: "resty rest".into(),
897
2
                }),
898
2
                ..default()
899
2
            },
900
2
        ],
901
    )?;
902

            
903
2
    t_err::<TopMinimal>(
904
2
        r#"test-item0 wrong # too many arguments @12
905
2
"#,
906
    )?;
907
2
    t_err::<TopMinimal>(
908
2
        r#"test-item0 # base64-encoded Object label is not as expected
909
2
-----BEGIN WRONG LABEL-----
910
2
aGVsbG8=
911
2
-----END WRONG LABEL-----
912
2
"#,
913
    )?;
914
2
    t_err::<TopMinimal>(
915
2
        r#"test-item0 # base64-encoded Object END label does not match BEGIN
916
2
-----BEGIN UTF-8 STRING-----
917
2
aGVsbG8=
918
2
-----END WRONG LABEL-----
919
2
"#,
920
    )?;
921
2
    t_err::<TopMinimal>(
922
2
        r#"test-item0
923
2
test-item-object-not-present # base64-encoded Object found where none expected
924
2
-----BEGIN TEST OBJECT-----
925
2
aGVsbG8=
926
2
-----END TEST OBJECT-----
927
2
"#,
928
    )?;
929
2
    t_err::<TopMinimal>(
930
2
        r#"test-item0 # base64-encoded Object has incorrectly formatted delimiter lines
931
2
-----BEGIN UTF-8 STRING-----
932
2
aGVsbG8=
933
2
-----END UTF-8 STRING
934
2
"#,
935
    )?;
936
2
    t_err::<TopMinimal>(
937
2
        r#"test-item0 # base64-encoded Object contains invalid base64
938
2
-----BEGIN UTF-8 STRING-----
939
2
bad b64 !
940
2
-----END UTF-8 STRING-----
941
2

            
942
2
"#,
943
    )?;
944
2
    t_err::<TopMinimal>(
945
2
        r#"test-item0 # base64-encoded Object contains invalid data
946
2
-----BEGIN UTF-8 STRING-----
947
2
hU6Qo2fW7+9PXkcrEyiB62ZDne/gwKPHXBo8lMeV8JCOfVBF5vT4BtKRLP+Jw66x
948
2
-----END UTF-8 STRING-----
949
2
"#,
950
    )?;
951

            
952
2
    Ok(())
953
2
}