1mod fields;
2mod parse_error;
3mod parsers;
4mod pgpcleartextmessage;
5mod raw_field;
6mod securitytxt;
7mod securitytxt_options;
8
9pub use fields::{
10 AcknowledgmentsField, CanonicalField, ContactField, EncryptionField, ExpiresField, ExtensionField, HiringField,
11 PolicyField, PreferredLanguagesField,
12};
13pub use parse_error::ParseError;
14pub use securitytxt::SecurityTxt;
15pub use securitytxt_options::SecurityTxtOptions;
16
17#[cfg(test)]
18mod tests {
19 use crate::fields::CsafField;
20
21 use super::*;
22 use chrono::{DateTime, Datelike, Duration, SecondsFormat, TimeZone, Utc};
23 use std::{fs, path::PathBuf};
24
25 const URL: &str = "https://securitytxt.org/";
26 const INSECURE_URL: &str = "http://securitytxt.org/";
27
28 fn some_datetime() -> DateTime<Utc> {
29 DateTime::parse_from_rfc3339("2023-01-01T08:19:03.000Z").unwrap().into()
30 }
31
32 fn future_expires_str() -> String {
33 (Utc::now() + Duration::days(365)).to_rfc3339_opts(SecondsFormat::Millis, true)
34 }
35
36 fn expires_dt(expires: &str) -> ExpiresField {
37 ExpiresField::new(expires, some_datetime()).unwrap()
38 }
39
40 fn get_parse_options() -> SecurityTxtOptions {
41 SecurityTxtOptions {
42 now: some_datetime(),
43 strict: true,
44 }
45 }
46
47 fn get_tests_dir(category: &str) -> PathBuf {
48 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
49 d.push(format!("resources/test/{category}"));
50 d
51 }
52
53 #[test]
54 fn test_contact_and_expires() {
55 let expires = future_expires_str();
56 let file = format!("Contact: {URL}\nExpires: {expires}\n");
57 let sec = SecurityTxt {
58 acknowledgments: vec![],
59 canonical: vec![],
60 contact: vec![ContactField::new(URL).unwrap()],
61 csaf: vec![],
62 encryption: vec![],
63 expires: expires_dt(&expires),
64 extension: vec![],
65 hiring: vec![],
66 policy: vec![],
67 preferred_languages: None,
68 };
69
70 assert_eq!(file.parse(), Ok(sec));
71 }
72
73 #[test]
74 fn test_comment() {
75 let expires = future_expires_str();
76 let file = format!("# this is a comment\n#\nContact: {URL}\nExpires: {expires}\n#\n");
77 let sec = SecurityTxt {
78 acknowledgments: vec![],
79 canonical: vec![],
80 contact: vec![ContactField::new(URL).unwrap()],
81 csaf: vec![],
82 encryption: vec![],
83 expires: expires_dt(&expires),
84 extension: vec![],
85 hiring: vec![],
86 policy: vec![],
87 preferred_languages: None,
88 };
89
90 assert_eq!(file.parse(), Ok(sec));
91 }
92
93 #[test]
94 fn test_newlines() {
95 let expires = future_expires_str();
96 let file = format!("\n\n\nContact: {URL}\n\n\nExpires: {expires}\n\n\n");
97 let sec = SecurityTxt {
98 acknowledgments: vec![],
99 canonical: vec![],
100 contact: vec![ContactField::new(URL).unwrap()],
101 csaf: vec![],
102 encryption: vec![],
103 expires: expires_dt(&expires),
104 extension: vec![],
105 hiring: vec![],
106 policy: vec![],
107 preferred_languages: None,
108 };
109
110 assert_eq!(file.parse(), Ok(sec));
111 }
112
113 #[test]
114 fn test_acknowledgements() {
115 let expires = future_expires_str();
116 let file = format!("Contact: {URL}\nExpires: {expires}\nAcknowledgments: {URL}\n");
117 let sec = SecurityTxt {
118 acknowledgments: vec![AcknowledgmentsField::new(URL).unwrap()],
119 canonical: vec![],
120 contact: vec![ContactField::new(URL).unwrap()],
121 csaf: vec![],
122 encryption: vec![],
123 expires: expires_dt(&expires),
124 extension: vec![],
125 hiring: vec![],
126 policy: vec![],
127 preferred_languages: None,
128 };
129
130 assert_eq!(file.parse(), Ok(sec));
131 }
132
133 #[test]
134 fn test_csaf() {
135 let expires = future_expires_str();
136 let file = format!("Contact: {URL}\nExpires: {expires}\nCSAF: {URL}\n");
137 let sec = SecurityTxt {
138 acknowledgments: vec![],
139 canonical: vec![],
140 contact: vec![ContactField::new(URL).unwrap()],
141 csaf: vec![CsafField::new(URL).unwrap()],
142 encryption: vec![],
143 expires: expires_dt(&expires),
144 extension: vec![],
145 hiring: vec![],
146 policy: vec![],
147 preferred_languages: None,
148 };
149
150 assert_eq!(file.parse(), Ok(sec));
151 }
152
153 #[test]
154 fn test_contact_missing() {
155 let expires = future_expires_str();
156 let file = format!("Expires: {expires}\n");
157
158 assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ContactFieldMissing));
159 }
160
161 #[test]
162 fn test_expires_missing() {
163 let file = format!("Contact: {URL}\n");
164
165 assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ExpiresFieldMissing));
166 }
167
168 #[test]
169 fn test_trailing_content() {
170 let expires = future_expires_str();
171 let file = format!("Contact: {URL}\nExpires: {expires}\nfoo");
172
173 assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::Malformed));
174 }
175
176 #[test]
177 fn test_preferred_languages() {
178 let expires = future_expires_str();
179 let file = format!("Contact: {URL}\nExpires: {expires}\nPreferred-Languages: en, fr\n");
180 let sec = SecurityTxt {
181 acknowledgments: vec![],
182 canonical: vec![],
183 contact: vec![ContactField::new(URL).unwrap()],
184 csaf: vec![],
185 encryption: vec![],
186 expires: expires_dt(&expires),
187 extension: vec![],
188 hiring: vec![],
189 policy: vec![],
190 preferred_languages: Some(PreferredLanguagesField::new("en, fr").unwrap()),
191 };
192
193 assert_eq!(file.parse::<SecurityTxt>(), Ok(sec));
194 }
195
196 #[test]
197 fn test_preferred_languages_multiple() {
198 let expires = future_expires_str();
199 let file = format!("Contact: {URL}\nExpires: {expires}\nPreferred-Languages: en\nPreferred-Languages: de\n");
200
201 assert_eq!(
202 file.parse::<SecurityTxt>(),
203 Err(ParseError::PreferredLanguagesFieldMultiple)
204 );
205 }
206
207 #[test]
208 fn test_expires_multiple() {
209 let expires = future_expires_str();
210 let file = format!("Contact: {URL}\nExpires: {expires}\nExpires: {expires}\n");
211
212 assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::ExpiresFieldMultiple));
213 }
214
215 #[test]
216 fn test_insecure_http() {
217 let expires = future_expires_str();
218 let file = format!("Contact: {INSECURE_URL}\nExpires: {expires}\n");
219
220 assert_eq!(file.parse::<SecurityTxt>(), Err(ParseError::InsecureHTTP));
221 }
222
223 #[test]
224 fn test_signed_contact() {
225 let expires = future_expires_str();
226 let file = format!(
227 "-----BEGIN PGP SIGNED MESSAGE-----\r
228Hash: SHA256\r
229\r
230Contact: {URL}
231Contact: {URL}\r
232Expires: {expires}\r
233-----BEGIN PGP SIGNATURE-----\r
234Version: GnuPG v2.2\r
235\r
236abcdefABCDEF/+==\r
237-----END PGP SIGNATURE-----\r
238"
239 );
240 let sec = SecurityTxt {
241 acknowledgments: vec![],
242 canonical: vec![],
243 contact: vec![ContactField::new(URL).unwrap(), ContactField::new(URL).unwrap()],
244 csaf: vec![],
245 encryption: vec![],
246 expires: expires_dt(&expires),
247 extension: vec![],
248 hiring: vec![],
249 policy: vec![],
250 preferred_languages: None,
251 };
252
253 assert_eq!(file.parse(), Ok(sec));
254 }
255
256 fn _test_category(category: &str) {
257 let paths = get_tests_dir(category).read_dir().unwrap();
258
259 for path in paths {
260 let buf = fs::read_to_string(path.unwrap().path()).unwrap();
261 let parse_options = get_parse_options();
262 let txt = SecurityTxt::parse_with(&buf, &parse_options);
263 assert_eq!(txt.is_ok(), true);
264 }
265 }
266
267 #[test]
268 fn test_category_valid_unsigned() {
269 _test_category("valid_unsigned")
270 }
271
272 #[test]
273 fn test_category_valid_signed() {
274 _test_category("valid_signed")
275 }
276
277 #[test]
278 fn test_category_gen_unsigned() {
279 _test_category("gen_unsigned")
280 }
281
282 #[test]
283 fn test_expires_non_z_time() {
284 let next_year = Utc::now().year() + 1;
285 let test_times = [
286 (
287 format!("{next_year}-08-30T00:00:00+00:00"),
288 Utc.with_ymd_and_hms(next_year, 8, 30, 0, 0, 0),
289 ),
290 (
291 format!("{next_year}-08-30T12:34:56+00:00"),
292 Utc.with_ymd_and_hms(next_year, 8, 30, 12, 34, 56),
293 ),
294 (
295 format!("{next_year}-08-30T02:00:00+02:00"),
296 Utc.with_ymd_and_hms(next_year, 8, 30, 0, 0, 0),
297 ),
298 (
299 format!("{next_year}-08-30T02:00:00-02:00"),
300 Utc.with_ymd_and_hms(next_year, 8, 30, 4, 0, 0),
301 ),
302 ];
303
304 for (expires_str, expected_dt) in &test_times {
305 let file = format!("Contact: {URL}\nExpires: {expires_str}\n");
306 let sec = SecurityTxt {
307 acknowledgments: vec![],
308 canonical: vec![],
309 contact: vec![ContactField::new(URL).unwrap()],
310 csaf: vec![],
311 encryption: vec![],
312 expires: ExpiresField::new(expires_str, some_datetime()).unwrap(),
313 extension: vec![],
314 hiring: vec![],
315 policy: vec![],
316 preferred_languages: None,
317 };
318
319 let parsed: SecurityTxt = file.parse().unwrap();
320 assert_eq!(parsed, sec);
321 let expected_dt = expected_dt.single().unwrap();
322 assert_eq!(parsed.expires.datetime.timestamp(), expected_dt.timestamp());
323 assert_eq!(
324 parsed.expires.datetime.timestamp_subsec_millis(),
325 expected_dt.timestamp_subsec_millis()
326 );
327 }
328 }
329}