1
//! `ConfigurationSources`: Helper for handling configuration files
2
//!
3
//! This module provides [`ConfigurationSources`].
4
//!
5
//! This layer brings together the functionality of
6
//! our underlying configuration library,
7
//! [`fs_mistrust`] and [`tor_config::cmdline`](crate::cmdline).
8
//!
9
//! A `ConfigurationSources` records a set of filenames of TOML files,
10
//! ancillary instructions for reading them,
11
//! and also a set of command line options.
12
//!
13
//! Usually, call [`ConfigurationSources::from_cmdline`],
14
//! perhaps [`set_mistrust`](ConfigurationSources::set_mistrust),
15
//! and finally [`load`](ConfigurationSources::load).
16
//! The resulting [`ConfigurationTree`] can then be deserialized.
17
//!
18
//! If you want to watch for config file changes,
19
//! use [`ConfigurationSources::scan()`],
20
//! to obtain a [`FoundConfigFiles`],
21
//! start watching the paths returned by [`FoundConfigFiles::iter()`],
22
//! and then call [`FoundConfigFiles::load()`].
23
//! (This ordering starts watching the files before you read them,
24
//! which is necessary to avoid possibly missing changes.)
25

            
26
use std::ffi::OsString;
27
use std::{fs, io, sync::Arc};
28

            
29
use figment::Figment;
30
use void::ResultVoidExt as _;
31

            
32
use crate::err::ConfigError;
33
use crate::{CmdLine, ConfigurationTree};
34

            
35
use std::path::{Path, PathBuf};
36

            
37
/// A description of where to find our configuration options.
38
#[derive(Clone, Debug, Default)]
39
pub struct ConfigurationSources {
40
    /// List of files to read (in order).
41
    files: Vec<(ConfigurationSource, MustRead)>,
42
    /// A list of command-line options to apply after parsing the files.
43
    options: Vec<String>,
44
    /// We will check all files we read
45
    mistrust: fs_mistrust::Mistrust,
46
}
47

            
48
/// Rules for whether we should proceed if a configuration file is unreadable.
49
///
50
/// Some files (like the default configuration file) are okay to skip if they
51
/// aren't present. Others (like those specified on the command line) really
52
/// need to be there.
53
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
54
#[allow(clippy::exhaustive_enums)]
55
pub enum MustRead {
56
    /// This file is okay to skip if it isn't present,
57
    TolerateAbsence,
58

            
59
    /// This file must be present and readable.
60
    MustRead,
61
}
62

            
63
/// A configuration file or directory, for use by a `ConfigurationSources`
64
///
65
/// You can make one out of a `PathBuf`, examining its syntax like `arti` does,
66
/// using `ConfigurationSource::from_path`.
67
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
68
#[allow(clippy::exhaustive_enums)]
69
pub enum ConfigurationSource {
70
    /// A plain file
71
    File(PathBuf),
72

            
73
    /// A directory
74
    Dir(PathBuf),
75

            
76
    /// A verbatim TOML file
77
    Verbatim(Arc<String>),
78
}
79

            
80
impl ConfigurationSource {
81
    /// Interpret a path (or string) as a configuration file or directory spec
82
    ///
83
    /// If the path syntactically specifies a directory
84
    /// (i.e., can be seen to be a directory without accessing the filesystem,
85
    /// for example because it ends in a directory separator such as `/`)
86
    /// it is treated as specifying a directory.
87
5778
    pub fn from_path<P: Into<PathBuf>>(p: P) -> ConfigurationSource {
88
        use ConfigurationSource as CS;
89
5778
        let p = p.into();
90
5778
        if is_syntactically_directory(&p) {
91
2014
            CS::Dir(p)
92
        } else {
93
3764
            CS::File(p)
94
        }
95
5778
    }
96

            
97
    /// Use the provided text as verbatim TOML, as if it had been read from disk.
98
508
    pub fn from_verbatim(text: String) -> ConfigurationSource {
99
508
        Self::Verbatim(Arc::new(text))
100
508
    }
101

            
102
    /// Return a reference to the inner `Path`, if there is one.
103
82
    pub fn as_path(&self) -> Option<&Path> {
104
        use ConfigurationSource as CS;
105
82
        match self {
106
82
            CS::File(p) | CS::Dir(p) => Some(p),
107
            CS::Verbatim(_) => None,
108
        }
109
82
    }
110
}
111

            
112
/// Configuration files and directories we found in the filesystem
113
///
114
/// Result of [`ConfigurationSources::scan`].
115
///
116
/// When loading configuration files and also watching for filesystem updates,
117
/// this type encapsulates all the actual filesystem objects that need watching.
118
#[derive(Debug)]
119
pub struct FoundConfigFiles<'srcs> {
120
    /// The things we found
121
    ///
122
    /// This includes both:
123
    ///  * Files which ought to be read
124
    ///  * Directories, which may or may not contain any currently-relevant files
125
    ///
126
    /// The directories are retained for the purpose of watching for config changes:
127
    /// we will want to detect files being created within them,
128
    /// so our caller needs to discover them (via [`FoundConfigFiles::iter()`]).
129
    files: Vec<FoundConfigFile>,
130

            
131
    /// Our parent, which contains details we need for `load`
132
    sources: &'srcs ConfigurationSources,
133
}
134

            
135
/// A configuration source file or directory, found or not found on the filesystem
136
#[derive(Debug, Clone)]
137
struct FoundConfigFile {
138
    /// The path of the (putative) object
139
    source: ConfigurationSource,
140

            
141
    /// Were we expecting this to definitely exist
142
    must_read: MustRead,
143
}
144

            
145
impl ConfigurationSources {
146
    /// Create a new empty [`ConfigurationSources`].
147
2420
    pub fn new_empty() -> Self {
148
2420
        Self::default()
149
2420
    }
150

            
151
    /// Establish a [`ConfigurationSources`] the from an infallible command line and defaults
152
    ///
153
    /// Convenience method for if the default config file location(s) can be infallibly computed.
154
4
    pub fn from_cmdline<F, O>(
155
4
        default_config_files: impl IntoIterator<Item = ConfigurationSource>,
156
4
        config_files_options: impl IntoIterator<Item = F>,
157
4
        cmdline_toml_override_options: impl IntoIterator<Item = O>,
158
4
    ) -> Self
159
4
    where
160
4
        F: Into<PathBuf>,
161
4
        O: Into<String>,
162
    {
163
4
        ConfigurationSources::try_from_cmdline(
164
2
            || Ok(default_config_files),
165
4
            config_files_options,
166
4
            cmdline_toml_override_options,
167
        )
168
4
        .void_unwrap()
169
4
    }
170

            
171
    /// Establish a [`ConfigurationSources`] the usual way from a command line and defaults
172
    ///
173
    /// The caller should have parsed the program's command line, and extracted (inter alia)
174
    ///
175
    ///  * `config_files_options`: Paths of config file(s) (or directories of `.toml` files)
176
    ///  * `cmdline_toml_override_options`: Overrides ("key=value")
177
    ///
178
    /// The caller should also provide `default_config_files`,
179
    /// which returns the default locations of the configuration files.
180
    /// This used if no file(s) are specified on the command line.
181
    //
182
    // The other inputs are always used and therefore
183
    // don't need to be lifted into FnOnce() -> Result.
184
    ///
185
    /// `ConfigurationSource::Dir`s
186
    /// will be scanned for files whose name ends in `.toml`.
187
    /// All those files (if any) will be read (in lexical order by filename).
188
310
    pub fn try_from_cmdline<F, O, DEF, E>(
189
310
        default_config_files: impl FnOnce() -> Result<DEF, E>,
190
310
        config_files_options: impl IntoIterator<Item = F>,
191
310
        cmdline_toml_override_options: impl IntoIterator<Item = O>,
192
310
    ) -> Result<Self, E>
193
310
    where
194
310
        F: Into<PathBuf>,
195
310
        O: Into<String>,
196
310
        DEF: IntoIterator<Item = ConfigurationSource>,
197
    {
198
310
        let mut cfg_sources = ConfigurationSources::new_empty();
199

            
200
310
        let mut any_files = false;
201
620
        for f in config_files_options {
202
310
            let f = f.into();
203
310
            cfg_sources.push_source(ConfigurationSource::from_path(f), MustRead::MustRead);
204
310
            any_files = true;
205
310
        }
206
310
        if !any_files {
207
2
            for default in default_config_files()? {
208
2
                cfg_sources.push_source(default, MustRead::TolerateAbsence);
209
2
            }
210
308
        }
211

            
212
600
        for s in cmdline_toml_override_options {
213
290
            cfg_sources.push_option(s);
214
290
        }
215

            
216
310
        Ok(cfg_sources)
217
310
    }
218

            
219
    /// Add `src` to the list of files or directories that we want to read configuration from.
220
    ///
221
    /// Configuration files are loaded and applied in the order that they are
222
    /// added to this object.
223
    ///
224
    /// If the listed file is absent, loading the configuration won't succeed.
225
2386
    pub fn push_source(&mut self, src: ConfigurationSource, must_read: MustRead) {
226
2386
        self.files.push((src, must_read));
227
2386
    }
228

            
229
    /// Add `s` to the list of overridden options to apply to our configuration.
230
    ///
231
    /// Options are applied after all configuration files are loaded, in the
232
    /// order that they are added to this object.
233
    ///
234
    /// The format for `s` is as in [`CmdLine`].
235
290
    pub fn push_option(&mut self, option: impl Into<String>) {
236
290
        self.options.push(option.into());
237
290
    }
238

            
239
    /// All given override options.
240
    ///
241
    /// See [`push_option`](Self::push_option).
242
    pub fn options(&self) -> impl Iterator<Item = &String> + Clone {
243
        self.options.iter()
244
    }
245

            
246
    /// Sets the filesystem permission mistrust
247
    ///
248
    /// This value only indicates whether and how to check permissions
249
    /// on the configuration file itself.
250
    /// It *does not* specify whether and how to check permissions of the
251
    /// paths provided within.
252
    /// This is defined by the `storage.permissions.dangerously_trust_everyone` flag.
253
1836
    pub fn set_mistrust(&mut self, mistrust: fs_mistrust::Mistrust) {
254
1836
        self.mistrust = mistrust;
255
1836
    }
256

            
257
    /// Reads the filesystem permission mistrust
258
    ///
259
    /// This value only indicates whether and how to check permissions
260
    /// on the configuration file itself.
261
    /// It *does not* specify whether and how to check permissions of the
262
    /// paths provided within.
263
    /// This is defined by the `storage.permissions.dangerously_trust_everyone` flag.
264
    pub fn mistrust(&self) -> &fs_mistrust::Mistrust {
265
        &self.mistrust
266
    }
267

            
268
    /// Scan for files and load the configuration into a new [`ConfigurationTree`].
269
    ///
270
    /// This is a convenience method for [`scan()`](Self::scan)
271
    /// followed by `files.load`.
272
2388
    pub fn load(&self) -> Result<ConfigurationTree, ConfigError> {
273
2388
        let files = self.scan()?;
274
2386
        files.load()
275
2388
    }
276

            
277
    /// Scan for configuration source files (including scanning any directories)
278
2534
    pub fn scan(&self) -> Result<FoundConfigFiles, ConfigError> {
279
2534
        let mut out = vec![];
280

            
281
5038
        for &(ref source, must_read) in &self.files {
282
2506
            let required = must_read == MustRead::MustRead;
283

            
284
            // Returns Err(error) if we should bail,
285
            // or Ok(()) if we should ignore the error and skip the file.
286
2508
            let handle_io_error = |e: io::Error, p: &Path| {
287
4
                if e.kind() == io::ErrorKind::NotFound && !required {
288
2
                    Result::<_, crate::ConfigError>::Ok(())
289
                } else {
290
2
                    Err(crate::ConfigError::Io {
291
2
                        action: "reading",
292
2
                        path: p.to_owned(),
293
2
                        err: Arc::new(e),
294
2
                    })
295
                }
296
4
            };
297

            
298
            use ConfigurationSource as CS;
299
2506
            match &source {
300
6
                CS::Dir(dirname) => {
301
6
                    let dir = match fs::read_dir(dirname) {
302
2
                        Ok(y) => y,
303
4
                        Err(e) => {
304
4
                            handle_io_error(e, dirname.as_ref())?;
305
2
                            continue;
306
                        }
307
                    };
308
2
                    out.push(FoundConfigFile {
309
2
                        source: source.clone(),
310
2
                        must_read,
311
2
                    });
312
                    // Rebinding `found` avoids using the directory name by mistake.
313
2
                    let mut entries = vec![];
314
6
                    for found in dir {
315
                        // reuse map_io_err, which embeds the directory name,
316
                        // since if we have Err we don't have an entry name.
317
4
                        let found = match found {
318
4
                            Ok(y) => y,
319
                            Err(e) => {
320
                                handle_io_error(e, dirname.as_ref())?;
321
                                continue;
322
                            }
323
                        };
324
4
                        let leaf = found.file_name();
325
4
                        let leaf: &Path = leaf.as_ref();
326
4
                        match leaf.extension() {
327
2
                            Some(e) if e == "toml" => {}
328
2
                            _ => continue,
329
                        }
330
2
                        entries.push(found.path());
331
                    }
332
2
                    entries.sort();
333
2
                    out.extend(entries.into_iter().map(|path| FoundConfigFile {
334
2
                        source: CS::File(path),
335
2
                        must_read: MustRead::TolerateAbsence,
336
2
                    }));
337
                }
338
2500
                CS::File(_) | CS::Verbatim(_) => {
339
2500
                    out.push(FoundConfigFile {
340
2500
                        source: source.clone(),
341
2500
                        must_read,
342
2500
                    });
343
2500
                }
344
            }
345
        }
346

            
347
2532
        Ok(FoundConfigFiles {
348
2532
            files: out,
349
2532
            sources: self,
350
2532
        })
351
2534
    }
352
}
353

            
354
impl FoundConfigFiles<'_> {
355
    /// Iterate over the filesystem objects that the scan found
356
    //
357
    // This ought really to be `impl IntoIterator for &Self` but that's awkward without TAIT
358
146
    pub fn iter(&self) -> impl Iterator<Item = &ConfigurationSource> {
359
146
        self.files.iter().map(|f| &f.source)
360
146
    }
361

            
362
    /// Add every file and commandline source to `builder`, returning a new
363
    /// builder.
364
2496
    fn add_sources(self, mut builder: Figment) -> Result<Figment, ConfigError> {
365
        use figment::providers::Format;
366

            
367
        // Note that we're using `merge` here.  It causes later sources' options
368
        // to replace those in earlier sources, and causes arrays to be replaced
369
        // rather than extended.
370
        //
371
        // TODO #1337: This array behavior is not necessarily ideal for all
372
        // cases, but doing something smarter would probably require us to hack
373
        // figment-rs or toml.
374

            
375
4964
        for FoundConfigFile { source, must_read } in self.files {
376
            use ConfigurationSource as CS;
377

            
378
2468
            let required = must_read == MustRead::MustRead;
379

            
380
2468
            let file = match source {
381
1958
                CS::File(file) => file,
382
2
                CS::Dir(_) => continue,
383
508
                CS::Verbatim(text) => {
384
508
                    builder = builder.merge(figment::providers::Toml::string(&text));
385
508
                    continue;
386
                }
387
            };
388

            
389
1958
            match self
390
1958
                .sources
391
1958
                .mistrust
392
1958
                .verifier()
393
1958
                .permit_readable()
394
1958
                .check(&file)
395
            {
396
1954
                Ok(()) => {}
397
4
                Err(fs_mistrust::Error::NotFound(_)) if !required => {
398
4
                    continue;
399
                }
400
                Err(e) => return Err(ConfigError::FileAccess(e)),
401
            }
402

            
403
            // We use file_exact here so that figment won't look in parent
404
            // directories if the target file can't be found.
405
1954
            let f = figment::providers::Toml::file_exact(file);
406
1954
            builder = builder.merge(f);
407
        }
408

            
409
2496
        let mut cmdline = CmdLine::new();
410
4190
        for opt in &self.sources.options {
411
1694
            cmdline.push_toml_line(opt.clone());
412
1694
        }
413
2496
        builder = builder.merge(cmdline);
414

            
415
2496
        Ok(builder)
416
2496
    }
417

            
418
    /// Load the configuration into a new [`ConfigurationTree`].
419
2496
    pub fn load(self) -> Result<ConfigurationTree, ConfigError> {
420
2496
        let mut builder = Figment::new();
421
2496
        builder = self.add_sources(builder)?;
422

            
423
2496
        Ok(ConfigurationTree(builder))
424
2496
    }
425
}
426

            
427
/// Does it end in a slash?  (Or some other way of saying this is a directory.)
428
6214
fn is_syntactically_directory(p: &Path) -> bool {
429
    use std::path::Component as PC;
430

            
431
6214
    match p.components().next_back() {
432
2
        None => false,
433
12
        Some(PC::Prefix(_)) | Some(PC::RootDir) | Some(PC::CurDir) | Some(PC::ParentDir) => true,
434
        Some(PC::Normal(_)) => {
435
            // Does it end in a slash?
436
6200
            let l = p.components().count();
437

            
438
            // stdlib doesn't let us tell if the thing ends in a path separator.
439
            // components() normalises, so doesn't give us an empty component
440
            // But, if it ends in a path separator, adding a path component char will
441
            // mean adding a component.
442
            // This will work regardless of the path separator, on any platform where
443
            // paths naming directories are like those for files.
444
            // It would even work on some others, eg VMS.
445
6200
            let mut appended = OsString::from(p);
446
6200
            appended.push("a");
447
6200
            let l2 = PathBuf::from(appended).components().count();
448
6200
            l2 != l
449
        }
450
    }
451
6214
}
452

            
453
#[cfg(test)]
454
mod test {
455
    // @@ begin test lint list maintained by maint/add_warning @@
456
    #![allow(clippy::bool_assert_comparison)]
457
    #![allow(clippy::clone_on_copy)]
458
    #![allow(clippy::dbg_macro)]
459
    #![allow(clippy::mixed_attributes_style)]
460
    #![allow(clippy::print_stderr)]
461
    #![allow(clippy::print_stdout)]
462
    #![allow(clippy::single_char_pattern)]
463
    #![allow(clippy::unwrap_used)]
464
    #![allow(clippy::unchecked_time_subtraction)]
465
    #![allow(clippy::useless_vec)]
466
    #![allow(clippy::needless_pass_by_value)]
467
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
468

            
469
    use super::*;
470
    use itertools::Itertools;
471
    use tempfile::tempdir;
472

            
473
    static EX_TOML: &str = "
474
[hello]
475
world = \"stuff\"
476
friends = 4242
477
";
478

            
479
    /// Make a ConfigurationSources (that doesn't include the arti defaults)
480
    fn sources_nodefaults<P: AsRef<Path>>(
481
        files: &[(P, MustRead)],
482
        opts: &[String],
483
    ) -> ConfigurationSources {
484
        let mistrust = fs_mistrust::Mistrust::new_dangerously_trust_everyone();
485
        let files = files
486
            .iter()
487
            .map(|(p, m)| (ConfigurationSource::from_path(p.as_ref()), *m))
488
            .collect_vec();
489
        let options = opts.iter().cloned().collect_vec();
490
        ConfigurationSources {
491
            files,
492
            options,
493
            mistrust,
494
        }
495
    }
496

            
497
    /// Load from a set of files and option strings, without taking
498
    /// the arti defaults into account.
499
    fn load_nodefaults<P: AsRef<Path>>(
500
        files: &[(P, MustRead)],
501
        opts: &[String],
502
    ) -> Result<ConfigurationTree, crate::ConfigError> {
503
        sources_nodefaults(files, opts).load()
504
    }
505

            
506
    #[test]
507
    fn non_required_file() {
508
        let td = tempdir().unwrap();
509
        let dflt = td.path().join("a_file");
510
        let files = vec![(dflt, MustRead::TolerateAbsence)];
511
        load_nodefaults(&files, Default::default()).unwrap();
512
    }
513

            
514
    static EX2_TOML: &str = "
515
[hello]
516
world = \"nonsense\"
517
";
518

            
519
    #[test]
520
    fn both_required_and_not() {
521
        let td = tempdir().unwrap();
522
        let dflt = td.path().join("a_file");
523
        let cf = td.path().join("other_file");
524
        std::fs::write(&cf, EX2_TOML).unwrap();
525
        let files = vec![(dflt, MustRead::TolerateAbsence), (cf, MustRead::MustRead)];
526
        let c = load_nodefaults(&files, Default::default()).unwrap();
527

            
528
        assert!(c.get_string("hello.friends").is_err());
529
        assert_eq!(c.get_string("hello.world").unwrap(), "nonsense");
530
    }
531

            
532
    #[test]
533
    fn dir_with_some() {
534
        let td = tempdir().unwrap();
535
        let cf = td.path().join("1.toml");
536
        let d = td.path().join("extra.d/");
537
        let df = d.join("2.toml");
538
        let xd = td.path().join("nonexistent.d/");
539
        std::fs::create_dir(&d).unwrap();
540
        std::fs::write(&cf, EX_TOML).unwrap();
541
        std::fs::write(df, EX2_TOML).unwrap();
542
        std::fs::write(d.join("not-toml"), "SYNTAX ERROR").unwrap();
543

            
544
        let files = vec![
545
            (cf, MustRead::MustRead),
546
            (d, MustRead::MustRead),
547
            (xd.clone(), MustRead::TolerateAbsence),
548
        ];
549
        let c = sources_nodefaults(&files, Default::default());
550
        let found = c.scan().unwrap();
551

            
552
        assert_eq!(
553
            found
554
                .iter()
555
                .map(|p| p
556
                    .as_path()
557
                    .unwrap()
558
                    .strip_prefix(&td)
559
                    .unwrap()
560
                    .to_str()
561
                    .unwrap())
562
                .collect_vec(),
563
            &["1.toml", "extra.d", "extra.d/2.toml"]
564
        );
565

            
566
        let c = found.load().unwrap();
567

            
568
        assert_eq!(c.get_string("hello.friends").unwrap(), "4242");
569
        assert_eq!(c.get_string("hello.world").unwrap(), "nonsense");
570

            
571
        let files = vec![(xd, MustRead::MustRead)];
572
        let e = load_nodefaults(&files, Default::default())
573
            .unwrap_err()
574
            .to_string();
575
        assert!(dbg!(e).contains("nonexistent.d"));
576
    }
577

            
578
    #[test]
579
    fn load_two_files_with_cmdline() {
580
        let td = tempdir().unwrap();
581
        let cf1 = td.path().join("a_file");
582
        let cf2 = td.path().join("other_file");
583
        std::fs::write(&cf1, EX_TOML).unwrap();
584
        std::fs::write(&cf2, EX2_TOML).unwrap();
585
        let v = vec![(cf1, MustRead::TolerateAbsence), (cf2, MustRead::MustRead)];
586
        let v2 = vec!["other.var=present".to_string()];
587
        let c = load_nodefaults(&v, &v2).unwrap();
588

            
589
        assert_eq!(c.get_string("hello.friends").unwrap(), "4242");
590
        assert_eq!(c.get_string("hello.world").unwrap(), "nonsense");
591
        assert_eq!(c.get_string("other.var").unwrap(), "present");
592
    }
593

            
594
    #[test]
595
    fn from_cmdline() {
596
        // Try one with specified files
597
        let sources = ConfigurationSources::from_cmdline(
598
            [ConfigurationSource::from_path("/etc/loid.toml")],
599
            ["/family/yor.toml", "/family/anya.toml"],
600
            ["decade=1960", "snack=peanuts"],
601
        );
602
        let files: Vec<_> = sources
603
            .files
604
            .iter()
605
            .map(|file| file.0.as_path().unwrap().to_str().unwrap())
606
            .collect();
607
        assert_eq!(files, vec!["/family/yor.toml", "/family/anya.toml"]);
608
        assert_eq!(sources.files[0].1, MustRead::MustRead);
609
        assert_eq!(
610
            &sources.options,
611
            &vec!["decade=1960".to_owned(), "snack=peanuts".to_owned()]
612
        );
613

            
614
        // Try once with default only.
615
        let sources = ConfigurationSources::from_cmdline(
616
            [ConfigurationSource::from_path("/etc/loid.toml")],
617
            Vec::<PathBuf>::new(),
618
            ["decade=1960", "snack=peanuts"],
619
        );
620
        assert_eq!(
621
            &sources.files,
622
            &vec![(
623
                ConfigurationSource::from_path("/etc/loid.toml"),
624
                MustRead::TolerateAbsence
625
            )]
626
        );
627
    }
628

            
629
    #[test]
630
    fn dir_syntax() {
631
        let chk = |tf, s: &str| assert_eq!(tf, is_syntactically_directory(s.as_ref()), "{:?}", s);
632

            
633
        chk(false, "");
634
        chk(false, "1");
635
        chk(false, "1/2");
636
        chk(false, "/1");
637
        chk(false, "/1/2");
638

            
639
        chk(true, "/");
640
        chk(true, ".");
641
        chk(true, "./");
642
        chk(true, "..");
643
        chk(true, "../");
644
        chk(true, "/");
645
        chk(true, "1/");
646
        chk(true, "1/2/");
647
        chk(true, "/1/");
648
        chk(true, "/1/2/");
649
    }
650
}