|
1 | 1 | use std::path::Path; |
2 | | -use std::time::UNIX_EPOCH; |
| 2 | +use std::time::{Duration, SystemTime, UNIX_EPOCH}; |
3 | 3 |
|
4 | 4 | use fluxheim_common::test_support::{safe_child_path, unique_temp_path}; |
5 | 5 |
|
@@ -51,13 +51,80 @@ fn directory_listing_local_time_uses_local_timestamp_shape() { |
51 | 51 | assert!(!html.contains("GMT")); |
52 | 52 | } |
53 | 53 |
|
| 54 | +#[test] |
| 55 | +fn directory_listing_rejects_pre_epoch_timestamps_without_panicking() { |
| 56 | + let modified = UNIX_EPOCH |
| 57 | + .checked_sub(Duration::from_secs(1)) |
| 58 | + .expect("test platform should represent a pre-epoch timestamp"); |
| 59 | + |
| 60 | + for local_time in [false, true] { |
| 61 | + let html = render_listing_with_timestamp(modified, local_time); |
| 62 | + assert!(html.contains("<td>1</td><td>-</td>"), "{html}"); |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +#[test] |
| 67 | +fn directory_listing_rejects_post_year_9999_timestamps_without_panicking() { |
| 68 | + let modified = UNIX_EPOCH |
| 69 | + .checked_add(Duration::from_secs(253_402_300_800)) |
| 70 | + .expect("test platform should represent a post-year-9999 timestamp"); |
| 71 | + |
| 72 | + for local_time in [false, true] { |
| 73 | + let html = render_listing_with_timestamp(modified, local_time); |
| 74 | + assert!(html.contains("<td>1</td><td>-</td>"), "{html}"); |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +fn render_listing_with_timestamp(modified: SystemTime, local_time: bool) -> String { |
| 79 | + render_directory_listing(&DirectoryListing { |
| 80 | + path: "/".to_owned(), |
| 81 | + entries: vec![DirectoryEntry { |
| 82 | + name: "timestamp.txt".to_owned(), |
| 83 | + is_dir: false, |
| 84 | + size: Some(1), |
| 85 | + modified: Some(modified), |
| 86 | + }], |
| 87 | + local_time, |
| 88 | + }) |
| 89 | +} |
| 90 | + |
54 | 91 | #[test] |
55 | 92 | fn safe_relative_path_rejects_non_normal_components() { |
56 | 93 | assert!(SafeRelativePath::from_path(Path::new("assets/app.css")).is_some()); |
57 | 94 | assert!(SafeRelativePath::from_path(Path::new("../secret")).is_none()); |
58 | 95 | assert!(SafeRelativePath::from_path(Path::new("/absolute")).is_none()); |
59 | 96 | } |
60 | 97 |
|
| 98 | +#[test] |
| 99 | +fn safe_relative_path_try_push_preserves_component_invariant() { |
| 100 | + let mut path = SafeRelativePath::default(); |
| 101 | + path.try_push("assets") |
| 102 | + .expect("normal component should pass"); |
| 103 | + |
| 104 | + for invalid in [ |
| 105 | + "", |
| 106 | + ".", |
| 107 | + "..", |
| 108 | + "../secret", |
| 109 | + "a/b", |
| 110 | + "/absolute", |
| 111 | + "trailing/", |
| 112 | + "nul\0byte", |
| 113 | + ] { |
| 114 | + assert!(path.try_push(invalid).is_err(), "accepted {invalid:?}"); |
| 115 | + } |
| 116 | + assert_eq!(path.as_path(), Path::new("assets")); |
| 117 | +} |
| 118 | + |
| 119 | +#[cfg(windows)] |
| 120 | +#[test] |
| 121 | +fn safe_relative_path_try_push_rejects_windows_separators() { |
| 122 | + let mut path = SafeRelativePath::default(); |
| 123 | + assert!(path.try_push(r"parent\child").is_err()); |
| 124 | + assert!(path.try_push(r"C:\absolute").is_err()); |
| 125 | + assert_eq!(path.as_path(), Path::new("")); |
| 126 | +} |
| 127 | + |
61 | 128 | #[test] |
62 | 129 | fn safe_relative_path_detects_prefixed_components() { |
63 | 130 | let path = SafeRelativePath::from_path(Path::new(".well-known/acme-challenge")) |
|
0 commit comments