asciidoc_parser/parser/path_resolver.rs
1use std::{fmt::Debug, sync::LazyLock};
2
3use regex::Regex;
4
5/// A `PathResolver` handles all operations for resolving, cleaning, and joining
6/// paths, covering both web paths (request URIs) and system paths.
7///
8/// The crate calls [`web_path`](Self::web_path) to turn an asset target (an
9/// image `src`, a stylesheet href, and so on) – optionally relative to a start
10/// path – into the clean, resolved path that appears in the rendered output.
11/// Clean paths are void of duplicate parent and current directory references in
12/// the path name.
13///
14/// This is one of the crate's extensibility seams, alongside
15/// [`IncludeFileHandler`], [`ImageFileHandler`], [`SvgFileHandler`],
16/// [`DocinfoFileHandler`], [`ReferenceResolver`], and
17/// [`InlineSubstitutionRenderer`]. A host that needs custom path or URL
18/// rewriting – a virtual filesystem, a content root, URL slugs (as an
19/// Antora- or Zola-style site generator might) – can supply its own
20/// implementation via [`Parser::with_path_resolver`]. The default
21/// implementation, [`DefaultPathResolver`], mirrors Ruby Asciidoctor's
22/// `PathResolver`.
23///
24/// [`IncludeFileHandler`]: crate::parser::IncludeFileHandler
25/// [`ImageFileHandler`]: crate::parser::ImageFileHandler
26/// [`SvgFileHandler`]: crate::parser::SvgFileHandler
27/// [`DocinfoFileHandler`]: crate::parser::DocinfoFileHandler
28/// [`ReferenceResolver`]: crate::parser::ReferenceResolver
29/// [`InlineSubstitutionRenderer`]: crate::parser::InlineSubstitutionRenderer
30/// [`Parser::with_path_resolver`]: crate::Parser::with_path_resolver
31pub trait PathResolver: Debug {
32 /// Resolve a web path from the target and start paths.
33 ///
34 /// The main function of this operation is to resolve any parent references
35 /// and remove any self references.
36 ///
37 /// The target is assumed to be a path, not a qualified URI. That check
38 /// should happen before this method is invoked.
39 ///
40 /// Returns a path that joins the target path with the start path with any
41 /// parent references resolved and self references removed.
42 fn web_path(&self, target: &str, start: Option<&str>) -> String;
43}
44
45/// The default [`PathResolver`] implementation, mirroring the Ruby Asciidoctor
46/// `PathResolver` class.
47///
48/// The main emphasis of the struct is on creating clean and secure paths. Clean
49/// paths are void of duplicate parent and current directory references in the
50/// path name. Secure paths are paths which are restricted from accessing
51/// directories outside of a jail path, if specified.
52///
53/// Since joining two paths can result in an insecure path, this struct also
54/// handles the task of joining a parent (start) and child (target) path.
55///
56/// Like its counterpart in the Ruby Asciidoctor implementation, this struct
57/// makes no use of path utilities from the underlying Rust libraries. Instead,
58/// it handles all aspects of path manipulation. The main benefit of
59/// internalizing these operations is that the struct is able to handle both
60/// Posix and Windows paths independent of the operating system on which it
61/// runs. This makes the class both deterministic and easier to test.
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct DefaultPathResolver {
64 /// File separator to use for path operations. (Defaults to
65 /// platform-appropriate separator.)
66 pub file_separator: char,
67 // Ruby's `PathResolver` also carries a `working_dir`, but it is read only by
68 // `system_path` – the filesystem-resolution + safe-mode-jail routine – as a
69 // fallback base directory, and Ruby always seeds it from `Dir.pwd`. This
70 // crate performs no filesystem I/O: `include::` and asset resolution are
71 // delegated to the client via `IncludeFileHandler`, so there is no consumer
72 // for `working_dir`, and a `Dir.pwd`-derived base would contradict this
73 // type's deterministic, OS-independent design. Intentionally not ported; if
74 // a built-in filesystem handler ever ports `system_path`, prefer an explicit
75 // caller-supplied base over an implicit working directory.
76}
77
78impl Default for DefaultPathResolver {
79 fn default() -> Self {
80 Self {
81 file_separator: std::path::MAIN_SEPARATOR,
82 }
83 }
84}
85
86impl PathResolver for DefaultPathResolver {
87 fn web_path(&self, target: &str, start: Option<&str>) -> String {
88 let mut target = self.posixify(target);
89 let start = start.map(|start| self.posixify(start));
90
91 let mut uri_prefix: Option<String> = None;
92
93 // Ruby treats a `nil` *or* empty `start` the same (`start.nil_or_empty?`):
94 // in both cases the target is used as-is, with no start path prepended.
95 let start_is_empty = start.as_deref().is_none_or(str::is_empty);
96
97 if !(start_is_empty || self.is_web_root(&target)) {
98 (target, uri_prefix) = extract_uri_prefix(&format!(
99 "{start}{maybe_add_slash}{target}",
100 start = start.as_deref().unwrap_or_default(),
101 maybe_add_slash = start
102 .as_ref()
103 .map(|s| if s.ends_with("/") { "" } else { "/" })
104 .unwrap_or_default()
105 ));
106 }
107
108 let (target_segments, target_root) = self.partition_path(&target, WebPath(true));
109
110 let mut resolved_segments: Vec<String> = vec![];
111
112 for segment in target_segments {
113 if segment == ".." {
114 if resolved_segments.is_empty() {
115 if let Some(target_root) = target_root.as_ref()
116 && target_root != "./"
117 {
118 // Do nothing.
119 } else {
120 resolved_segments.push(segment);
121 }
122 } else if let Some(last_segment) = resolved_segments.last()
123 && last_segment == ".."
124 {
125 resolved_segments.push(segment);
126 } else {
127 resolved_segments.pop();
128 }
129 } else {
130 resolved_segments.push(segment);
131 }
132 }
133
134 let resolved_path = self
135 .join_path(&resolved_segments, target_root.as_deref())
136 .replace(" ", "%20");
137
138 format!(
139 "{uri_prefix}{resolved_path}",
140 uri_prefix = uri_prefix.unwrap_or_default()
141 )
142 }
143}
144
145impl DefaultPathResolver {
146 /// Normalize path by converting any backslashes to forward slashes.
147 pub fn posixify(&self, path: &str) -> String {
148 if self.file_separator == '\\' && path.contains('\\') {
149 path.replace('\\', "/")
150 } else {
151 path.to_string()
152 }
153 }
154
155 /// Partition the path into path segments and remove self references (`.`)
156 /// and the trailing slash, if present. Prior to being partitioned, the path
157 /// is converted to a Posix path.
158 ///
159 /// Parent references are not resolved by this method since the caller often
160 /// needs to handle this resolution in a certain context (checking for the
161 /// breach of a jail, for instance).
162 ///
163 /// Returns a 2-item tuple containing a `Vec<String>` of path segments and
164 /// an optional path root (e.g., `/`, `./`, `c:/`, or `//`), which is only
165 /// present if the path is absolute.
166 fn partition_path(&self, path: &str, web: WebPath) -> (Vec<String>, Option<String>) {
167 // Ruby memoizes partition results per (path, web) in a hash. That cache
168 // exists to avoid Ruby's comparatively expensive string work; here the
169 // partitioning is cheap and the resolver takes `&self`, so caching would
170 // require interior mutability that the derived `Clone`/`Eq` would then
171 // have to skip. Deliberately not ported.
172 let posix_path = self.posixify(path);
173
174 let root: Option<String> = if web.0 {
175 if self.is_web_root(&posix_path) {
176 Some("/".to_owned())
177 } else if posix_path.starts_with("./") {
178 Some("./".to_owned())
179 } else {
180 None
181 }
182 } else if self.is_root(&posix_path) {
183 if self.is_unc(&posix_path) {
184 // ex. //sample/path
185 Some(DOUBLE_SLASH.to_owned())
186 } else if posix_path.starts_with('/') {
187 // ex. /sample/path
188 Some("/".to_owned())
189 } else if posix_path.starts_with(URI_CLASSLOADER) {
190 // ex. uri:classloader:sample/path (or uri:classloader:/sample/path)
191 Some(URI_CLASSLOADER.to_owned())
192 } else {
193 // ex. C:/sample/path (or file:///sample/path in browser
194 // environment). The root is everything up to and including the
195 // first slash. `is_root` guarantees a slash for the drive-letter
196 // case, so `None` here is unreachable in practice.
197 posix_path
198 .find('/')
199 .map(|slash| posix_path[..=slash].to_owned())
200 }
201 } else if posix_path.starts_with("./") {
202 // ex. ./sample/path
203 Some("./".to_owned())
204 } else {
205 // otherwise ex. sample/path
206 None
207 };
208
209 let path_after_root = if let Some(root) = &root {
210 &posix_path[root.len()..]
211 } else {
212 &posix_path
213 };
214
215 let mut path_segments: Vec<String> = path_after_root
216 .split('/')
217 .filter(|s| *s != ".")
218 .map(|s| s.to_owned())
219 .collect();
220
221 // Ruby builds these segments with `posix_path.split SLASH`, and Ruby's
222 // `String#split` drops trailing empty fields (e.g. `'a/b/'.split '/'` is
223 // `['a', 'b']`, and `''.split '/'` is `[]`). Rust's `str::split` keeps
224 // them, so strip trailing empties to match – otherwise a trailing slash
225 // would survive as a spurious empty final segment.
226 while path_segments.last().is_some_and(|s| s.is_empty()) {
227 path_segments.pop();
228 }
229
230 (path_segments, root)
231 }
232
233 /// Join the segments using the Posix file separator (since this crate knows
234 /// how to work with paths specified this way, regardless of OS). Use the
235 /// `root`, if specified, to construct an absolute path. Otherwise join the
236 /// segments as a relative path.
237 fn join_path(&self, segments: &[String], root: Option<&str>) -> String {
238 format!(
239 "{root}{segments}",
240 root = root.unwrap_or_default(),
241 segments = segments.join("/"),
242 )
243 }
244
245 /// Return `true` if the path is an absolute (root) web path (i.e. starts
246 /// with a `'/'`.
247 pub fn is_web_root(&self, path: &str) -> bool {
248 path.starts_with('/')
249 }
250
251 /// Return `true` if the path is an absolute (rooted) system path. A path is
252 /// absolute if it starts with `'/'` or, on a Windows-style resolver, if it
253 /// starts with a drive letter or slash root (e.g. `C:/` or `\`).
254 fn is_absolute_path(&self, path: &str) -> bool {
255 path.starts_with('/') || (self.file_separator == '\\' && WINDOWS_ROOT.is_match(path))
256 }
257
258 /// Return `true` if the path has a root, meaning it is either an absolute
259 /// path or a `uri:classloader:` path.
260 fn is_root(&self, path: &str) -> bool {
261 self.is_absolute_path(path) || path.starts_with(URI_CLASSLOADER)
262 }
263
264 /// Return `true` if the path is a UNC path (i.e. starts with `'//'`).
265 fn is_unc(&self, path: &str) -> bool {
266 path.starts_with(DOUBLE_SLASH)
267 }
268}
269
270/// Efficiently extracts the URI prefix from the specified string if the string
271/// is a URI.
272///
273/// Attempts to match the URI prefix in the specified string (e.g., `http://`). If present, the prefix is removed.
274///
275/// Returns a tuple containing the specified string without the URI prefix, if
276/// present, and the extracted URI prefix if found.
277fn extract_uri_prefix(s: &str) -> (String, Option<String>) {
278 if s.contains(':')
279 && let Some(prefix) = URI_SNIFF.find(s)
280 {
281 (
282 s[prefix.len()..].to_string(),
283 Some(prefix.as_str().to_owned()),
284 )
285 } else {
286 (s.to_string(), None)
287 }
288}
289
290// Also: Place this at module scope:
291static URI_SNIFF: LazyLock<Regex> = LazyLock::new(|| {
292 #[allow(clippy::unwrap_used)]
293 Regex::new(
294 r#"(?x)
295 ^ # Anchor: start of string
296
297 \p{Alphabetic} # First character: a Unicode letter
298
299 [\p{Alphabetic} # Followed by one or more of:
300 \p{Number} # - Unicode letters or numbers
301 . # - Period
302 \+ # - Plus sign
303 \- # - Hyphen
304 ]+ # One or more of the above
305
306 : # Followed by a literal colon
307
308 /{0,2} # Followed by zero, one, or two literal slashes
309 "#,
310 )
311 .unwrap()
312});
313
314/// Path root marker for a UNC path (e.g. `//sample/path`).
315const DOUBLE_SLASH: &str = "//";
316
317/// Path root marker for a JRuby classloader URI (e.g.
318/// `uri:classloader:/sample/path`).
319const URI_CLASSLOADER: &str = "uri:classloader:";
320
321/// Matches the root of a Windows-style path: an optional drive letter followed
322/// by a leading slash (either separator). Mirrors Ruby Asciidoctor's
323/// `WindowsRootRx`.
324static WINDOWS_ROOT: LazyLock<Regex> = LazyLock::new(|| {
325 #[allow(clippy::unwrap_used)]
326 Regex::new(r"^(?:[a-zA-Z]:)?[\\/]").unwrap()
327});
328
329#[derive(Clone, Debug, Eq, PartialEq)]
330pub(crate) struct WebPath(pub(crate) bool);
331
332#[cfg(test)]
333mod tests {
334 #![allow(clippy::unwrap_used)]
335
336 use crate::parser::DefaultPathResolver;
337
338 mod posixify {
339 use crate::parser::DefaultPathResolver;
340
341 #[test]
342 fn replaces_backslashes_if_windowsish() {
343 let pr = DefaultPathResolver {
344 file_separator: '\\',
345 };
346
347 assert_eq!(pr.posixify("abc/def\\ghi"), "abc/def/ghi");
348 }
349
350 #[test]
351 fn doesnt_replace_backslashes_if_posixish() {
352 let pr = DefaultPathResolver {
353 file_separator: '/',
354 };
355
356 assert_eq!(pr.posixify("abc/def\\ghi"), "abc/def\\ghi");
357 }
358
359 #[test]
360 fn doesnt_replace_backslashes_if_none_exist() {
361 let pr = DefaultPathResolver {
362 file_separator: '\\',
363 };
364
365 assert_eq!(pr.posixify("abc/def"), "abc/def");
366 }
367 }
368
369 mod web_path {
370 use crate::parser::{DefaultPathResolver, PathResolver};
371
372 #[test]
373 fn test_cases_from_asciidoctor_rb() {
374 let pr = DefaultPathResolver::default();
375
376 assert_eq!(pr.web_path("images", None), "images");
377 assert_eq!(pr.web_path("./images", None), "./images");
378 assert_eq!(pr.web_path("/images", None), "/images");
379
380 assert_eq!(
381 pr.web_path("./images/../assets/images", None),
382 "./assets/images"
383 );
384
385 assert_eq!(pr.web_path("/../images", None), "/images");
386
387 assert_eq!(pr.web_path("/../images", Some("assets")), "/images");
388 assert_eq!(pr.web_path("../images", Some("./")), "./../images");
389 assert_eq!(pr.web_path("../../images", Some("./")), "./../../images");
390
391 assert_eq!(
392 pr.web_path("tiger.png", Some("../assets/images")),
393 "../assets/images/tiger.png"
394 );
395
396 // Basic relative path resolution.
397 assert_eq!(
398 pr.web_path("images/photo.jpg", Some("docs/guide")),
399 "docs/guide/images/photo.jpg"
400 );
401 assert_eq!(pr.web_path("photo.jpg", Some("images")), "images/photo.jpg");
402 assert_eq!(
403 pr.web_path("../photo.jpg", Some("images/folder")),
404 "images/photo.jpg"
405 );
406 assert_eq!(
407 pr.web_path("../../photo.jpg", Some("docs/images/folder")),
408 "docs/photo.jpg"
409 );
410
411 // URI-based scenarios (triggers `extract_uri_prefix`).
412 assert_eq!(
413 pr.web_path("images/photo.jpg", Some("http://example.com/base")),
414 "http://example.com/base/images/photo.jpg"
415 );
416 assert_eq!(
417 pr.web_path("../images/logo.png", Some("https://cdn.example.com/assets")),
418 "https://cdn.example.com/images/logo.png"
419 );
420 assert_eq!(
421 pr.web_path("docs/guide.pdf", Some("file:///Users/docs")),
422 "file:///Users/docs/docs/guide.pdf"
423 );
424 assert_eq!(
425 pr.web_path("assets/style.css", Some("ftp://files.example.com/web")),
426 "ftp://files.example.com/web/assets/style.css"
427 );
428
429 // Web root scenarios (start parameter ignored).
430 assert_eq!(
431 pr.web_path("/absolute/path.jpg", Some("http://example.com/base")),
432 "/absolute/path.jpg"
433 );
434 assert_eq!(
435 pr.web_path("/images/photo.jpg", Some("docs/guide")),
436 "/images/photo.jpg"
437 );
438 assert_eq!(pr.web_path("/", Some("any/path")), "/");
439
440 // No start path scenarios.
441 assert_eq!(pr.web_path("images/photo.jpg", None), "images/photo.jpg");
442 assert_eq!(pr.web_path("../photo.jpg", None), "../photo.jpg");
443
444 // Path normalization with dots.
445 assert_eq!(
446 pr.web_path("./photo.jpg", Some("images")),
447 "images/photo.jpg"
448 );
449 assert_eq!(
450 pr.web_path("folder/./photo.jpg", Some("images")),
451 "images/folder/photo.jpg"
452 );
453 assert_eq!(
454 pr.web_path("folder/../photo.jpg", Some("images")),
455 "images/photo.jpg"
456 );
457
458 // Complex path resolution.
459 assert_eq!(
460 pr.web_path("../../../photo.jpg", Some("docs/images/folder/sub")),
461 "docs/photo.jpg"
462 );
463 assert_eq!(
464 pr.web_path("folder/../../photo.jpg", Some("docs/images")),
465 "docs/photo.jpg"
466 );
467 assert_eq!(
468 pr.web_path("./folder/../photo.jpg", Some("images")),
469 "images/photo.jpg"
470 );
471
472 // Edge cases with trailing slashes.
473 assert_eq!(
474 pr.web_path("photo.jpg", Some("images/")),
475 "images/photo.jpg"
476 );
477 assert_eq!(pr.web_path("photo.jpg", Some("images")), "images/photo.jpg");
478
479 // URLs with paths and parent references.
480 assert_eq!(
481 pr.web_path("../styles/main.css", Some("https://example.com/assets/css")),
482 "https://example.com/assets/styles/main.css"
483 );
484 assert_eq!(
485 pr.web_path(
486 "../../images/logo.png",
487 Some("http://site.com/docs/guide/examples")
488 ),
489 "http://site.com/docs/images/logo.png"
490 );
491
492 // Space handling (gets URL encoded).
493 assert_eq!(
494 pr.web_path("my file.jpg", Some("images")),
495 "images/my%20file.jpg"
496 );
497 assert_eq!(
498 pr.web_path("folder with spaces/file.jpg", Some("docs")),
499 "docs/folder%20with%20spaces/file.jpg"
500 );
501
502 // Protocol-less absolute paths.
503 assert_eq!(
504 pr.web_path(
505 "//cdn.example.com/assets/image.jpg",
506 Some("http://example.com")
507 ),
508 "//cdn.example.com/assets/image.jpg"
509 );
510
511 // Mixed scenarios. (An empty target resolves to the start path with
512 // no spurious trailing slash, and an empty `start` is treated like
513 // `None` – matching Ruby's `web_path`; see `paths_test.rb`.)
514 assert_eq!(pr.web_path("", Some("docs/images")), "docs/images");
515 assert_eq!(pr.web_path("", Some("")), "");
516 assert_eq!(pr.web_path("", None), "");
517
518 // Complex URI scenarios.
519 assert_eq!(
520 pr.web_path("api/v1/data", Some("https://api.example.com:8080/base")),
521 "https://api.example.com:8080/base/api/v1/data"
522 );
523 assert_eq!(
524 pr.web_path("../v2/data", Some("https://api.example.com/api/v1")),
525 "https://api.example.com/api/v2/data"
526 );
527
528 // File protocol variations.
529 assert_eq!(
530 pr.web_path("document.pdf", Some("file:///C:/Users/docs")),
531 "file:///C:/Users/docs/document.pdf"
532 );
533 assert_eq!(
534 pr.web_path("../shared/doc.pdf", Some("file:///home/user/documents")),
535 "file:///home/user/shared/doc.pdf"
536 );
537 }
538
539 #[test]
540 fn all_slash_target_collapses_to_root() {
541 // An all-slash web target normalizes to a single `/`. Ruby's
542 // `String#split('/')` drops *all* trailing empty fields, so
543 // `partition_path` yields no segments and the rooted path collapses
544 // to `/`. This guards the trailing-empty-segment stripping in
545 // `partition_path` against regressing to a one-shot pop (which would
546 // leave `//`, `///`, etc.).
547 let pr = DefaultPathResolver::default();
548
549 assert_eq!(pr.web_path("/", None), "/");
550 assert_eq!(pr.web_path("//", None), "/");
551 assert_eq!(pr.web_path("///", None), "/");
552 assert_eq!(pr.web_path("////", None), "/");
553 assert_eq!(pr.web_path("///", Some("assets")), "/");
554 }
555 }
556
557 #[test]
558 fn is_web_root() {
559 let pr = DefaultPathResolver::default();
560 assert!(pr.is_web_root("/blah"));
561 assert!(!pr.is_web_root(""));
562 assert!(!pr.is_web_root("./blah"));
563 }
564
565 mod partition_path {
566 use super::super::WebPath;
567 use crate::parser::DefaultPathResolver;
568
569 fn seg(items: &[&str]) -> Vec<String> {
570 items.iter().map(|s| (*s).to_owned()).collect()
571 }
572
573 #[test]
574 fn relative_system_path() {
575 let pr = DefaultPathResolver::default();
576 assert_eq!(
577 pr.partition_path("sample/path", WebPath(false)),
578 (seg(&["sample", "path"]), None)
579 );
580 }
581
582 #[test]
583 fn dot_slash_system_path() {
584 let pr = DefaultPathResolver::default();
585 assert_eq!(
586 pr.partition_path("./sample/path", WebPath(false)),
587 (seg(&["sample", "path"]), Some("./".to_owned()))
588 );
589 }
590
591 #[test]
592 fn self_references_are_removed() {
593 let pr = DefaultPathResolver::default();
594 assert_eq!(
595 pr.partition_path("sample/./path", WebPath(false)),
596 (seg(&["sample", "path"]), None)
597 );
598 }
599
600 #[test]
601 fn absolute_system_path() {
602 let pr = DefaultPathResolver::default();
603 assert_eq!(
604 pr.partition_path("/sample/path", WebPath(false)),
605 (seg(&["sample", "path"]), Some("/".to_owned()))
606 );
607 }
608
609 #[test]
610 fn unc_path() {
611 let pr = DefaultPathResolver::default();
612 assert_eq!(
613 pr.partition_path("//server/share/path", WebPath(false)),
614 (seg(&["server", "share", "path"]), Some("//".to_owned()))
615 );
616 }
617
618 #[test]
619 fn uri_classloader_rooted() {
620 let pr = DefaultPathResolver::default();
621 assert_eq!(
622 pr.partition_path("uri:classloader:/sample/path", WebPath(false)),
623 (
624 seg(&["", "sample", "path"]),
625 Some("uri:classloader:".to_owned())
626 )
627 );
628 }
629
630 #[test]
631 fn uri_classloader_relative() {
632 let pr = DefaultPathResolver::default();
633 assert_eq!(
634 pr.partition_path("uri:classloader:sample/path", WebPath(false)),
635 (
636 seg(&["sample", "path"]),
637 Some("uri:classloader:".to_owned())
638 )
639 );
640 }
641
642 #[test]
643 fn windows_drive_letter() {
644 let pr = DefaultPathResolver {
645 file_separator: '\\',
646 };
647
648 assert_eq!(
649 pr.partition_path("C:\\sample\\path", WebPath(false)),
650 (seg(&["sample", "path"]), Some("C:/".to_owned()))
651 );
652 }
653
654 #[test]
655 fn windows_drive_letter_forward_slashes() {
656 let pr = DefaultPathResolver {
657 file_separator: '\\',
658 };
659
660 assert_eq!(
661 pr.partition_path("C:/sample/path", WebPath(false)),
662 (seg(&["sample", "path"]), Some("C:/".to_owned()))
663 );
664 }
665
666 #[test]
667 fn drive_letter_is_relative_on_posix_resolver() {
668 // Without a Windows-style separator, a drive-letter prefix is not
669 // treated as a root. (Constructed explicitly rather than via
670 // `default()`, whose separator is platform-dependent.)
671 let pr = DefaultPathResolver {
672 file_separator: '/',
673 };
674
675 assert_eq!(
676 pr.partition_path("C:/sample/path", WebPath(false)),
677 (seg(&["C:", "sample", "path"]), None)
678 );
679 }
680 }
681}