1use std::path::Path;
4use std::time::Duration;
5
6use reqwest::Url;
7use reqwest::blocking::Client;
8use saphyr::LoadableYamlNode;
9use saphyr::MarkedYaml;
10use saphyr::Scalar;
11use saphyr::YamlData;
12use url::Url as ParseUrl;
13
14use crate::Error;
15use crate::Number;
16use crate::Result;
17use crate::RootSchema;
18use crate::schemas::BooleanOrSchema;
19use crate::schemas::YamlSchema;
20use crate::utils::format_marker;
21use crate::utils::try_unwrap_saphyr_scalar;
22
23pub fn load_file<S: AsRef<str>>(path: S) -> Result<RootSchema> {
27 let fs_metadata = std::fs::metadata(path.as_ref())?;
28 if !fs_metadata.is_file() {
29 return Err(Error::FileNotFound(path.as_ref().to_string()));
30 }
31 let s = std::fs::read_to_string(path.as_ref())?;
32 let mut root = load_from_str(&s)?;
33 let canonical = Path::new(path.as_ref()).canonicalize()?;
34 root.base_uri = Some(
35 ParseUrl::from_file_path(canonical)
36 .map_err(|_| Error::GenericError("Failed to convert file path to URL".to_string()))?,
37 );
38 Ok(root)
39}
40
41pub fn load_from_str(s: &str) -> Result<RootSchema> {
43 let docs = MarkedYaml::load_from_str(s).map_err(Error::YamlParsingError)?;
44 load_from_docs(docs)
45}
46
47pub fn load_from_docs<'f>(docs: Vec<MarkedYaml<'f>>) -> Result<RootSchema> {
49 let Some(first_doc) = docs.first() else {
50 return Ok(RootSchema::empty());
51 };
52 load_from_doc(first_doc)
53}
54
55pub fn load_from_doc<'f>(doc: &MarkedYaml<'f>) -> Result<RootSchema> {
57 RootSchema::try_from(doc)
58}
59
60#[derive(thiserror::Error, Debug)]
62pub enum UrlLoadError {
63 #[error("Failed to download from URL: {0}")]
64 DownloadError(#[from] reqwest::Error),
65
66 #[error("Failed to parse URL: {0}")]
67 ParseUrlError(#[from] url::ParseError),
68
69 #[error("Failed to parse YAML: {0}")]
70 ParseError(#[from] saphyr::ScanError),
71
72 #[error("No YAML documents found in the downloaded content")]
73 NoDocuments,
74}
75
76impl From<reqwest::Error> for crate::Error {
77 fn from(value: reqwest::Error) -> Self {
78 crate::Error::UrlLoadError(UrlLoadError::DownloadError(value))
79 }
80}
81
82pub fn load_from_content(content: &str, base_uri: Option<ParseUrl>) -> Result<RootSchema> {
84 let docs = MarkedYaml::load_from_str(content).map_err(Error::YamlParsingError)?;
85 let doc = docs
86 .first()
87 .ok_or_else(|| crate::generic_error!("No YAML documents in content"))?;
88 let mut root = load_from_doc(doc)?;
89 root.base_uri = base_uri;
90 Ok(root)
91}
92
93pub fn load_external_schema(doc_url: &str) -> Result<RootSchema> {
95 let parsed = ParseUrl::parse(doc_url).map_err(|e| Error::UrlLoadError(e.into()))?;
96 match parsed.scheme() {
97 "file" => {
98 let path = parsed
99 .to_file_path()
100 .map_err(|_| Error::GenericError("Invalid file URL".to_string()))?;
101 let path_str = path
102 .to_str()
103 .ok_or_else(|| Error::GenericError("Non-UTF-8 file path".to_string()))?;
104 load_file(path_str)
105 }
106 "http" | "https" => {
107 let (content, url) = fetch_url(doc_url, None)?;
108 load_from_content(&content, Some(url))
109 }
110 _ => Err(Error::GenericError(format!(
111 "Unsupported URL scheme for $ref: {}",
112 parsed.scheme()
113 ))),
114 }
115}
116
117pub fn extract_dollar_schema_from_yaml(contents: &str) -> Result<Option<String>> {
122 let docs = MarkedYaml::load_from_str(contents).map_err(Error::YamlParsingError)?;
123 let Some(first) = docs.first() else {
124 return Ok(None);
125 };
126 match &first.data {
127 YamlData::Mapping(mapping) => {
128 let key = MarkedYaml::value_from_str("$schema");
129 match mapping.get(&key) {
130 Some(v) => Ok(Some(marked_yaml_to_string(v, "$schema must be a string")?)),
131 None => Ok(None),
132 }
133 }
134 _ => Ok(None),
135 }
136}
137
138pub fn load_root_schema_from_ref(
144 schema_ref: &str,
145 instance_parent: &Path,
146) -> Result<(RootSchema, String)> {
147 let trimmed = schema_ref.trim();
148 if trimmed.is_empty() {
149 return Err(crate::generic_error!("$schema value is empty"));
150 }
151
152 let root = match ParseUrl::parse(trimmed) {
153 Ok(parsed) if matches!(parsed.scheme(), "http" | "https" | "file") => {
154 load_external_schema(trimmed)?
155 }
156 Ok(parsed) => {
157 return Err(crate::generic_error!(
158 "Unsupported URL scheme in $schema: {}",
159 parsed.scheme()
160 ));
161 }
162 Err(_) => {
163 let path = Path::new(trimmed);
164 let resolved = if path.is_absolute() {
165 path.to_path_buf()
166 } else {
167 instance_parent.join(path)
168 };
169 let path_str = resolved
170 .to_str()
171 .ok_or_else(|| Error::GenericError("Non-UTF-8 schema path".to_string()))?;
172 load_file(path_str)?
173 }
174 };
175
176 let fallback = root
177 .base_uri
178 .as_ref()
179 .map(|u| u.to_string())
180 .ok_or_else(|| {
181 Error::GenericError("Internal error: loaded schema missing base URI".to_string())
182 })?;
183
184 Ok((root, fallback))
185}
186
187pub fn fetch_url(url_string: &str, timeout_seconds: Option<u64>) -> Result<(String, Url)> {
192 let url_owned = url_string.to_string();
193 let timeout = Duration::from_secs(timeout_seconds.unwrap_or(30));
194
195 std::thread::spawn(move || {
196 let client = Client::builder()
197 .timeout(timeout)
198 .use_native_tls()
199 .build()?;
200
201 let url = Url::parse(&url_owned).map_err(|e| Error::UrlLoadError(e.into()))?;
202
203 let response = client.get(url.clone()).send()?;
204 if !response.status().is_success() {
205 match response.error_for_status() {
206 Ok(_) => unreachable!(),
207 Err(e) => return Err(e.into()),
208 }
209 }
210
211 let content = response.text()?;
212 Ok((content, url))
213 })
214 .join()
215 .unwrap_or_else(|_| {
216 Err(Error::GenericError(
217 "HTTP fetch thread panicked".to_string(),
218 ))
219 })
220}
221
222pub fn download_from_url(url_string: &str, timeout_seconds: Option<u64>) -> Result<RootSchema> {
238 let (yaml_content, url) = fetch_url(url_string, timeout_seconds)?;
239
240 let docs = MarkedYaml::load_from_str(&yaml_content).map_err(UrlLoadError::ParseError)?;
242
243 match docs.first() {
244 Some(doc) => {
245 let mut root = load_from_doc(doc)?;
246 root.base_uri = Some(url);
247 Ok(root)
248 }
249 None => Err(UrlLoadError::NoDocuments.into()),
250 }
251}
252
253pub fn marked_yaml_to_string<S: Into<String> + Copy>(yaml: &MarkedYaml, msg: S) -> Result<String> {
254 if let YamlData::Value(Scalar::String(s)) = &yaml.data {
255 Ok(s.to_string())
256 } else {
257 Err(Error::ExpectedScalar(msg.into()))
258 }
259}
260
261pub fn load_array_of_schemas_marked<'f>(value: &MarkedYaml<'f>) -> Result<Vec<YamlSchema>> {
262 if let YamlData::Sequence(values) = &value.data {
263 values
264 .iter()
265 .map(|v| {
266 if v.is_mapping() {
267 v.try_into()
268 } else {
269 Err(generic_error!("Expected a mapping, but got: {:?}", v))
270 }
271 })
272 .collect::<Result<Vec<YamlSchema>>>()
273 } else {
274 Err(generic_error!(
275 "{} Expected a sequence, but got: {:?}",
276 format_marker(&value.span.start),
277 value
278 ))
279 }
280}
281
282pub fn load_integer(value: &saphyr::Yaml) -> Result<i64> {
283 let scalar = try_unwrap_saphyr_scalar(value)?;
284 match scalar {
285 saphyr::Scalar::Integer(i) => Ok(*i),
286 _ => Err(unsupported_type!(
287 "Expected type: integer, but got: {:?}",
288 value
289 )),
290 }
291}
292
293pub fn load_integer_marked(value: &MarkedYaml) -> Result<i64> {
294 if let YamlData::Value(Scalar::Integer(i)) = &value.data {
295 Ok(*i)
296 } else {
297 Err(generic_error!(
298 "{} Expected integer value, got: {:?}",
299 format_marker(&value.span.start),
300 value
301 ))
302 }
303}
304
305pub fn load_number(value: &saphyr::Yaml) -> Result<Number> {
306 let scalar = try_unwrap_saphyr_scalar(value)?;
307 match scalar {
308 Scalar::Integer(i) => Ok(Number::integer(*i)),
309 Scalar::FloatingPoint(o) => Ok(Number::float(o.into_inner())),
310 _ => Err(unsupported_type!(
311 "Expected type: integer or float, but got: {:?}",
312 value
313 )),
314 }
315}
316
317pub fn load_array_items_marked<'input>(value: &MarkedYaml<'input>) -> Result<BooleanOrSchema> {
318 match &value.data {
319 YamlData::Value(scalar) => {
320 if let Scalar::Boolean(b) = scalar {
321 Ok(BooleanOrSchema::Boolean(*b))
322 } else {
323 Err(generic_error!(
324 "array: boolean or mapping with type or $ref, but got: {:?}",
325 value
326 ))
327 }
328 }
329 YamlData::Mapping(_mapping) => {
330 let schema: YamlSchema = value.try_into()?;
331 Ok(BooleanOrSchema::schema(schema))
332 }
333 _ => Err(generic_error!(
334 "array: boolean or mapping with type or $ref, but got: {:?}",
335 value
336 )),
337 }
338}
339
340pub fn load_boolean_or_schema_marked(value: &MarkedYaml<'_>) -> Result<BooleanOrSchema> {
342 match &value.data {
343 YamlData::Value(scalar) => match scalar {
344 Scalar::Boolean(b) => Ok(BooleanOrSchema::Boolean(*b)),
345 _ => Err(generic_error!(
346 "{} Expected a boolean scalar, but got: {:?}",
347 format_marker(&value.span.start),
348 scalar
349 )),
350 },
351 YamlData::Mapping(_) => {
352 let schema: YamlSchema = value.try_into()?;
353 Ok(BooleanOrSchema::schema(schema))
354 }
355 _ => Err(unsupported_type!(
356 "Expected boolean or mapping, but got: {:?}",
357 value
358 )),
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use regex::Regex;
365 use saphyr::LoadableYamlNode;
366 use saphyr::MarkedYaml;
367
368 use crate::ConstValue;
369 use crate::Engine;
370 use crate::Result;
371 use crate::Validator as _;
372 use crate::loader;
373 use crate::schemas::EnumSchema;
374 use crate::schemas::IntegerSchema;
375 use crate::schemas::SchemaType;
376 use crate::schemas::StringSchema;
377
378 use super::*;
379
380 #[test]
381 fn test_boolean_literal_true() {
382 let root_schema = load_from_doc(&MarkedYaml::value_from_str("true")).unwrap();
383 assert_eq!(root_schema.schema, YamlSchema::BooleanLiteral(true));
384 }
385
386 #[test]
387 fn test_boolean_literal_false() {
388 let root_schema = load_from_doc(&MarkedYaml::value_from_str("false")).unwrap();
389 assert_eq!(root_schema.schema, YamlSchema::BooleanLiteral(false));
390 }
391
392 #[test]
393 fn test_const_string() {
394 let docs = MarkedYaml::load_from_str("const: string value").unwrap();
395 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
396 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
397 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
398 };
399 assert_eq!(subschema.r#const, Some(ConstValue::string("string value")));
400 }
401
402 #[test]
403 fn test_const_integer() {
404 let docs = MarkedYaml::load_from_str("const: 42").unwrap();
405 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
406 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
407 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
408 };
409 assert_eq!(subschema.r#const, Some(ConstValue::integer(42)));
410 }
411
412 #[test]
413 fn test_const_array() {
414 let docs = MarkedYaml::load_from_str("const: [1, 2]").unwrap();
415 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
416 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
417 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
418 };
419 let expected = ConstValue::Array(vec![ConstValue::integer(1), ConstValue::integer(2)]);
420 assert_eq!(subschema.r#const, Some(expected));
421 }
422
423 #[test]
424 fn test_const_object() {
425 let docs = MarkedYaml::load_from_str("const:\n a: 1").unwrap();
426 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
427 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
428 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
429 };
430 let mut expected_obj = hashlink::LinkedHashMap::new();
431 expected_obj.insert("a".into(), ConstValue::integer(1));
432 assert_eq!(subschema.r#const, Some(ConstValue::Object(expected_obj)));
433 }
434
435 #[test]
436 fn test_type_foo_should_error() {
437 let docs = MarkedYaml::load_from_str("type: foo").unwrap();
438 let root_schema = load_from_doc(docs.first().unwrap());
439 assert!(root_schema.is_err());
440 assert_eq!(
441 root_schema.unwrap_err().to_string(),
442 "Unsupported type: Expected type: string, number, integer, object, array, boolean, or null, but got: foo"
443 );
444 }
445
446 #[test]
447 fn test_type_string() {
448 let docs = MarkedYaml::load_from_str("type: string").unwrap();
449 let root_schema = load_from_doc(docs.first().unwrap()).unwrap();
450 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
451 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
452 };
453 assert_eq!(subschema.r#type, SchemaType::new("string"));
454 }
455
456 #[test]
457 fn test_type_object_with_string_with_description() {
458 let root_schema = loader::load_from_str(
459 r#"
460 type: object
461 properties:
462 name:
463 type: string
464 description: This is a description
465 "#,
466 )
467 .expect("Failed to load schema");
468 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
469 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
470 };
471 let Some(object_schema) = &subschema.object_schema else {
472 panic!(
473 "Expected ObjectSchema, but got: {:?}",
474 &subschema.object_schema
475 );
476 };
477 let name_property = object_schema
478 .properties
479 .as_ref()
480 .expect("Expected properties")
481 .get("name")
482 .expect("Expected `name` property");
483
484 let YamlSchema::Subschema(name_property_schema) = &name_property else {
485 panic!(
486 "Expected Subschema for `name` property, but got: {:?}",
487 &name_property
488 );
489 };
490 assert_eq!(name_property_schema.r#type, SchemaType::new("string"));
491 assert_eq!(
492 name_property_schema.string_schema,
493 Some(StringSchema::default())
494 );
495 assert_eq!(
496 name_property_schema.metadata_and_annotations.description,
497 Some("This is a description".to_string())
498 );
499 }
500
501 #[test]
502 fn test_type_string_with_pattern() {
503 let root_schema = loader::load_from_str(
504 r#"
505 type: string
506 pattern: "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
507 "#,
508 )
509 .unwrap();
510 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
511 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
512 };
513 assert_eq!(subschema.r#type, SchemaType::new("string"));
514 let expected = StringSchema {
515 pattern: Some(Regex::new("^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$").unwrap()),
516 ..Default::default()
517 };
518
519 assert_eq!(subschema.string_schema, Some(expected));
520 }
521
522 #[test]
523 fn test_integer_schema() {
524 let root_schema = loader::load_from_str("type: integer").unwrap();
525 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
526 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
527 };
528 let integer_schema = IntegerSchema::default();
529 assert_eq!(subschema.integer_schema, Some(integer_schema));
530 }
531
532 #[test]
533 fn test_enum() {
534 let root_schema = loader::load_from_str(
535 r#"
536 enum:
537 - foo
538 - bar
539 - baz
540 "#,
541 )
542 .unwrap();
543 let enum_values = ["foo", "bar", "baz"]
544 .iter()
545 .map(|s| ConstValue::string(s.to_string()))
546 .collect::<Vec<ConstValue>>();
547 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
548 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
549 };
550 assert_eq!(
551 subschema.r#enum,
552 Some(EnumSchema {
553 r#enum: enum_values
554 })
555 );
556 }
557
558 #[test]
559 fn test_enum_without_type() {
560 let root_schema = loader::load_from_str(
561 r#"
562 enum:
563 - red
564 - amber
565 - green
566 - null
567 - 42
568 "#,
569 )
570 .unwrap();
571 let enum_values = vec![
572 ConstValue::string("red".to_string()),
573 ConstValue::string("amber".to_string()),
574 ConstValue::string("green".to_string()),
575 ConstValue::null(),
576 ConstValue::integer(42),
577 ];
578 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
579 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
580 };
581 assert_eq!(
582 subschema.r#enum,
583 Some(EnumSchema {
584 r#enum: enum_values
585 })
586 );
587 }
588
589 #[test]
590 fn test_defs() {
591 let root_schema = loader::load_from_str(
592 r##"
593 $defs:
594 foo:
595 type: boolean
596 "##,
597 )
598 .unwrap();
599 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
600 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
601 };
602 assert!(subschema.defs.is_some());
603 let Some(defs) = &subschema.defs else {
604 panic!("Expected defs, but got: {:?}", &subschema.defs);
605 };
606 assert_eq!(defs.len(), 1);
607 assert_eq!(defs.get("foo"), Some(&YamlSchema::typed_boolean()));
608 }
609
610 #[test]
611 fn test_one_of_with_ref() {
612 let root_schema = loader::load_from_str(
613 r##"
614 $defs:
615 foo:
616 type: boolean
617 oneOf:
618 - type: string
619 - $ref: "#/$defs/foo"
620 "##,
621 )
622 .unwrap();
623 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
624 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
625 };
626 assert!(subschema.one_of.is_some());
627 let Some(one_of) = &subschema.one_of else {
628 panic!("Expected oneOf, but got: {:?}", &subschema.one_of);
629 };
630 assert_eq!(one_of.one_of.len(), 2);
631 assert_eq!(
632 one_of.one_of[0],
633 YamlSchema::typed_string(StringSchema::default()),
634 "one_of[0] should be a string schema"
635 );
636 assert_eq!(
637 one_of.one_of[1],
638 YamlSchema::ref_str("#/$defs/foo"),
639 "one_of[1] should be a reference to '#/$defs/foo'"
640 );
641
642 let s = r#"
643 false
644 "#;
645 let docs = MarkedYaml::load_from_str(s).unwrap();
646 let value = docs.first().unwrap();
647 let context = crate::Context::with_root_schema(&root_schema, true);
648 let result = root_schema.validate(&context, value);
649 assert!(result.is_ok());
650 assert!(!context.has_errors());
651 }
652
653 #[test]
654 fn extract_dollar_schema_from_mapping() {
655 let yaml = "$schema: ./x.yaml\nfoo: 1\n";
656 assert_eq!(
657 extract_dollar_schema_from_yaml(yaml).unwrap(),
658 Some("./x.yaml".to_string())
659 );
660 }
661
662 #[test]
663 fn extract_dollar_schema_missing() {
664 assert_eq!(extract_dollar_schema_from_yaml("foo: 1\n").unwrap(), None);
665 }
666
667 #[test]
668 fn extract_dollar_schema_non_mapping_root() {
669 assert_eq!(extract_dollar_schema_from_yaml("- a\n").unwrap(), None);
670 }
671
672 #[test]
673 fn extract_dollar_schema_not_string_errors() {
674 let result = extract_dollar_schema_from_yaml("$schema: 42\n");
675 assert!(result.is_err());
676 }
677
678 #[test]
679 fn load_root_schema_from_ref_relative_path() {
680 let dir = std::env::temp_dir().join(format!("yaml_schema_ref_test_{}", std::process::id()));
681 std::fs::create_dir_all(&dir).expect("create temp dir");
682 let schema_path = dir.join("sch.yaml");
683 std::fs::write(
684 &schema_path,
685 "type: object\nproperties:\n a:\n type: string\n",
686 )
687 .expect("write schema");
688 let (root, uri) = load_root_schema_from_ref("sch.yaml", &dir).expect("load");
689 assert!(uri.starts_with("file://"));
690 let YamlSchema::Subschema(sub) = &root.schema else {
691 panic!("expected Subschema");
692 };
693 assert_eq!(sub.r#type, SchemaType::new("object"));
694 std::fs::remove_dir_all(&dir).ok();
695 }
696
697 #[test]
698 fn test_self_validate() -> Result<()> {
699 let schema_filename = "yaml-schema.yaml";
700 let root_schema = match loader::load_file(schema_filename) {
701 Ok(schema) => schema,
702 Err(e) => {
703 eprintln!("Failed to read YAML schema file: {schema_filename}");
704 log::error!("{e}");
705 return Err(e);
706 }
707 };
708
709 let yaml_contents = std::fs::read_to_string(schema_filename)?;
710
711 let context = Engine::evaluate(&root_schema, &yaml_contents, false)?;
712 if context.has_errors() {
713 for error in context.errors.borrow().iter() {
714 eprintln!("{error}");
715 }
716 }
717 assert!(!context.has_errors());
718
719 Ok(())
720 }
721
722 #[test]
723 fn test_download_from_url() {
724 if std::env::var("CI").is_ok() {
726 return;
728 }
729
730 let result = std::panic::catch_unwind(|| {
731 let url = "https://yaml-schema.net/yaml-schema.yaml";
732 let result = download_from_url(url, Some(10));
733
734 let root_schema = result.expect("Failed to download and parse YAML schema from URL");
736
737 let YamlSchema::Subschema(subschema) = &root_schema.schema else {
739 panic!("Expected Subschema, but got: {:?}", &root_schema.schema);
740 };
741 assert_eq!(subschema.r#type, SchemaType::new("object"));
742 assert!(subschema.object_schema.is_some());
743
744 if let Ok(local_schema) = std::fs::read_to_string("yaml-schema.yaml") {
746 let context = Engine::evaluate(&root_schema, &local_schema, false);
747 if let Ok(ctx) = context {
748 if ctx.has_errors() {
749 for error in ctx.errors.borrow().iter() {
750 eprintln!("Validation error: {}", error);
751 }
752 panic!("Downloaded schema failed validation against local schema");
753 }
754 } else if let Err(e) = context {
755 panic!("Failed to validate downloaded schema: {}", e);
756 }
757 }
758 });
759
760 if let Err(e) = result {
761 if let Some(s) = e.downcast_ref::<String>()
763 && (s.contains("Network is unreachable")
764 || s.contains("failed to lookup address information"))
765 {
766 eprintln!("Warning: Network unreachable, skipping download test");
767 return;
768 }
769
770 std::panic::resume_unwind(e);
772 }
773 }
774}