nt_time/file_time/
str.rs

1// SPDX-FileCopyrightText: 2023 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Utilities for string related operations.
6
7use core::str::FromStr;
8
9use super::FileTime;
10use crate::error::ParseFileTimeError;
11
12impl FileTime {
13    /// Parses a `FileTime` from a string slice with digits in a given base.
14    ///
15    /// The string is expected to be an optional `+` sign followed by only
16    /// digits. Leading and trailing non-digit characters (including whitespace)
17    /// represent an error. Underscores (which are accepted in Rust literals)
18    /// also represent an error.
19    ///
20    /// Digits are a subset of these characters, depending on `radix`:
21    ///
22    /// - `0-9`
23    /// - `a-z`
24    /// - `A-Z`
25    ///
26    /// # Errors
27    ///
28    /// Returns [`Err`] if [`u64::from_str_radix`] returns an error.
29    ///
30    /// # Panics
31    ///
32    /// Panics if `radix` is not in the range from 2 to 36.
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// # use nt_time::FileTime;
38    /// #
39    /// assert_eq!(
40    ///     FileTime::from_str_radix("0", 2),
41    ///     Ok(FileTime::NT_TIME_EPOCH)
42    /// );
43    /// assert_eq!(
44    ///     FileTime::from_str_radix("6355435732517500000", 8),
45    ///     Ok(FileTime::UNIX_EPOCH)
46    /// );
47    /// assert_eq!(
48    ///     FileTime::from_str_radix("+9223372036854775807", 10),
49    ///     Ok(FileTime::SIGNED_MAX)
50    /// );
51    /// assert_eq!(
52    ///     FileTime::from_str_radix("+ffffffffffffffff", 16),
53    ///     Ok(FileTime::MAX)
54    /// );
55    ///
56    /// assert!(FileTime::from_str_radix("8", 8).is_err());
57    ///
58    /// assert!(FileTime::from_str_radix("", 16).is_err());
59    ///
60    /// assert!(FileTime::from_str_radix("Z", 16).is_err());
61    /// assert!(FileTime::from_str_radix("_", 16).is_err());
62    /// assert!(FileTime::from_str_radix("-1", 16).is_err());
63    /// assert!(FileTime::from_str_radix("+", 16).is_err());
64    /// assert!(FileTime::from_str_radix("0 ", 16).is_err());
65    ///
66    /// assert!(FileTime::from_str_radix("3W5E11264SGSG", 36).is_err());
67    /// ```
68    ///
69    /// `radix` must be greater than or equal to 2:
70    ///
71    /// ```should_panic
72    /// # use nt_time::FileTime;
73    /// #
74    /// let _ = FileTime::from_str_radix("0", 1);
75    /// ```
76    ///
77    /// `radix` must be less than or equal to 36:
78    ///
79    /// ```should_panic
80    /// # use nt_time::FileTime;
81    /// #
82    /// let _ = FileTime::from_str_radix("0", 37);
83    /// ```
84    #[inline]
85    pub fn from_str_radix(src: &str, radix: u32) -> Result<Self, ParseFileTimeError> {
86        u64::from_str_radix(src, radix)
87            .map_err(ParseFileTimeError::new)
88            .map(Self::new)
89    }
90}
91
92impl FromStr for FileTime {
93    type Err = ParseFileTimeError;
94
95    /// Parses a string `src` to return a value of `FileTime`.
96    ///
97    /// <div class="warning">
98    ///
99    /// The string is expected to be a decimal non-negative integer. If the
100    /// string is not a decimal integer, use [`FileTime::from_str_radix`]
101    /// instead.
102    ///
103    /// </div>
104    ///
105    /// # Errors
106    ///
107    /// Returns [`Err`] if [`u64::from_str`] returns an error.
108    ///
109    /// # Examples
110    ///
111    /// ```
112    /// # use core::str::FromStr;
113    /// #
114    /// # use nt_time::FileTime;
115    /// #
116    /// assert_eq!(FileTime::from_str("0"), Ok(FileTime::NT_TIME_EPOCH));
117    /// assert_eq!(
118    ///     FileTime::from_str("116444736000000000"),
119    ///     Ok(FileTime::UNIX_EPOCH)
120    /// );
121    /// assert_eq!(
122    ///     FileTime::from_str("+9223372036854775807"),
123    ///     Ok(FileTime::SIGNED_MAX)
124    /// );
125    /// assert_eq!(
126    ///     FileTime::from_str("+18446744073709551615"),
127    ///     Ok(FileTime::MAX)
128    /// );
129    ///
130    /// assert!(FileTime::from_str("").is_err());
131    ///
132    /// assert!(FileTime::from_str("a").is_err());
133    /// assert!(FileTime::from_str("_").is_err());
134    /// assert!(FileTime::from_str("-1").is_err());
135    /// assert!(FileTime::from_str("+").is_err());
136    /// assert!(FileTime::from_str("0 ").is_err());
137    ///
138    /// assert!(FileTime::from_str("18446744073709551616").is_err());
139    /// ```
140    #[inline]
141    fn from_str(src: &str) -> Result<Self, Self::Err> {
142        Self::from_str_radix(src, 10)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use core::{
149        error::Error,
150        num::{IntErrorKind, ParseIntError},
151    };
152
153    use super::*;
154
155    #[test]
156    fn from_str_radix() {
157        assert_eq!(
158            FileTime::from_str_radix("0", 2).unwrap(),
159            FileTime::NT_TIME_EPOCH
160        );
161        assert_eq!(
162            FileTime::from_str_radix("+0", 2).unwrap(),
163            FileTime::NT_TIME_EPOCH
164        );
165        assert_eq!(
166            FileTime::from_str_radix(
167                "110011101101100011101111011010101001111101000000000000000",
168                2
169            )
170            .unwrap(),
171            FileTime::UNIX_EPOCH
172        );
173        assert_eq!(
174            FileTime::from_str_radix(
175                "+110011101101100011101111011010101001111101000000000000000",
176                2
177            )
178            .unwrap(),
179            FileTime::UNIX_EPOCH
180        );
181        assert_eq!(
182            FileTime::from_str_radix(
183                "111111111111111111111111111111111111111111111111111111111111111",
184                2
185            )
186            .unwrap(),
187            FileTime::SIGNED_MAX
188        );
189        assert_eq!(
190            FileTime::from_str_radix(
191                "+111111111111111111111111111111111111111111111111111111111111111",
192                2
193            )
194            .unwrap(),
195            FileTime::SIGNED_MAX
196        );
197        assert_eq!(
198            FileTime::from_str_radix(
199                "1111111111111111111111111111111111111111111111111111111111111111",
200                2
201            )
202            .unwrap(),
203            FileTime::MAX
204        );
205        assert_eq!(
206            FileTime::from_str_radix(
207                "+1111111111111111111111111111111111111111111111111111111111111111",
208                2
209            )
210            .unwrap(),
211            FileTime::MAX
212        );
213        assert_eq!(
214            FileTime::from_str_radix("0", 8).unwrap(),
215            FileTime::NT_TIME_EPOCH
216        );
217        assert_eq!(
218            FileTime::from_str_radix("+0", 8).unwrap(),
219            FileTime::NT_TIME_EPOCH
220        );
221        assert_eq!(
222            FileTime::from_str_radix("6355435732517500000", 8).unwrap(),
223            FileTime::UNIX_EPOCH
224        );
225        assert_eq!(
226            FileTime::from_str_radix("+6355435732517500000", 8).unwrap(),
227            FileTime::UNIX_EPOCH
228        );
229        assert_eq!(
230            FileTime::from_str_radix("777777777777777777777", 8).unwrap(),
231            FileTime::SIGNED_MAX
232        );
233        assert_eq!(
234            FileTime::from_str_radix("+777777777777777777777", 8).unwrap(),
235            FileTime::SIGNED_MAX
236        );
237        assert_eq!(
238            FileTime::from_str_radix("1777777777777777777777", 8).unwrap(),
239            FileTime::MAX
240        );
241        assert_eq!(
242            FileTime::from_str_radix("+1777777777777777777777", 8).unwrap(),
243            FileTime::MAX
244        );
245        assert_eq!(
246            FileTime::from_str_radix("0", 10).unwrap(),
247            FileTime::NT_TIME_EPOCH
248        );
249        assert_eq!(
250            FileTime::from_str_radix("+0", 10).unwrap(),
251            FileTime::NT_TIME_EPOCH
252        );
253        assert_eq!(
254            FileTime::from_str_radix("116444736000000000", 10).unwrap(),
255            FileTime::UNIX_EPOCH
256        );
257        assert_eq!(
258            FileTime::from_str_radix("+116444736000000000", 10).unwrap(),
259            FileTime::UNIX_EPOCH
260        );
261        assert_eq!(
262            FileTime::from_str_radix("9223372036854775807", 10).unwrap(),
263            FileTime::SIGNED_MAX
264        );
265        assert_eq!(
266            FileTime::from_str_radix("+9223372036854775807", 10).unwrap(),
267            FileTime::SIGNED_MAX
268        );
269        assert_eq!(
270            FileTime::from_str_radix("18446744073709551615", 10).unwrap(),
271            FileTime::MAX
272        );
273        assert_eq!(
274            FileTime::from_str_radix("+18446744073709551615", 10).unwrap(),
275            FileTime::MAX
276        );
277        assert_eq!(
278            FileTime::from_str_radix("0", 16).unwrap(),
279            FileTime::NT_TIME_EPOCH
280        );
281        assert_eq!(
282            FileTime::from_str_radix("+0", 16).unwrap(),
283            FileTime::NT_TIME_EPOCH
284        );
285        assert_eq!(
286            FileTime::from_str_radix("19db1ded53e8000", 16).unwrap(),
287            FileTime::UNIX_EPOCH
288        );
289        assert_eq!(
290            FileTime::from_str_radix("19DB1DED53E8000", 16).unwrap(),
291            FileTime::UNIX_EPOCH
292        );
293        assert_eq!(
294            FileTime::from_str_radix("+19db1ded53e8000", 16).unwrap(),
295            FileTime::UNIX_EPOCH
296        );
297        assert_eq!(
298            FileTime::from_str_radix("+19DB1DED53E8000", 16).unwrap(),
299            FileTime::UNIX_EPOCH
300        );
301        assert_eq!(
302            FileTime::from_str_radix("7fffffffffffffff", 16).unwrap(),
303            FileTime::SIGNED_MAX
304        );
305        assert_eq!(
306            FileTime::from_str_radix("7FFFFFFFFFFFFFFF", 16).unwrap(),
307            FileTime::SIGNED_MAX
308        );
309        assert_eq!(
310            FileTime::from_str_radix("+7fffffffffffffff", 16).unwrap(),
311            FileTime::SIGNED_MAX
312        );
313        assert_eq!(
314            FileTime::from_str_radix("+7FFFFFFFFFFFFFFF", 16).unwrap(),
315            FileTime::SIGNED_MAX
316        );
317        assert_eq!(
318            FileTime::from_str_radix("ffffffffffffffff", 16).unwrap(),
319            FileTime::MAX
320        );
321        assert_eq!(
322            FileTime::from_str_radix("FFFFFFFFFFFFFFFF", 16).unwrap(),
323            FileTime::MAX
324        );
325        assert_eq!(
326            FileTime::from_str_radix("+ffffffffffffffff", 16).unwrap(),
327            FileTime::MAX
328        );
329        assert_eq!(
330            FileTime::from_str_radix("+FFFFFFFFFFFFFFFF", 16).unwrap(),
331            FileTime::MAX
332        );
333        assert_eq!(
334            FileTime::from_str_radix("0", 36).unwrap(),
335            FileTime::NT_TIME_EPOCH
336        );
337        assert_eq!(
338            FileTime::from_str_radix("+0", 36).unwrap(),
339            FileTime::NT_TIME_EPOCH
340        );
341        assert_eq!(
342            FileTime::from_str_radix("vuk7p84etc0", 36).unwrap(),
343            FileTime::UNIX_EPOCH
344        );
345        assert_eq!(
346            FileTime::from_str_radix("VUK7P84ETC0", 36).unwrap(),
347            FileTime::UNIX_EPOCH
348        );
349        assert_eq!(
350            FileTime::from_str_radix("+vuk7p84etc0", 36).unwrap(),
351            FileTime::UNIX_EPOCH
352        );
353        assert_eq!(
354            FileTime::from_str_radix("+VUK7P84ETC0", 36).unwrap(),
355            FileTime::UNIX_EPOCH
356        );
357        assert_eq!(
358            FileTime::from_str_radix("1y2p0ij32e8e7", 36).unwrap(),
359            FileTime::SIGNED_MAX
360        );
361        assert_eq!(
362            FileTime::from_str_radix("1Y2P0IJ32E8E7", 36).unwrap(),
363            FileTime::SIGNED_MAX
364        );
365        assert_eq!(
366            FileTime::from_str_radix("+1y2p0ij32e8e7", 36).unwrap(),
367            FileTime::SIGNED_MAX
368        );
369        assert_eq!(
370            FileTime::from_str_radix("+1Y2P0IJ32E8E7", 36).unwrap(),
371            FileTime::SIGNED_MAX
372        );
373        assert_eq!(
374            FileTime::from_str_radix("3w5e11264sgsf", 36).unwrap(),
375            FileTime::MAX
376        );
377        assert_eq!(
378            FileTime::from_str_radix("3W5E11264SGSF", 36).unwrap(),
379            FileTime::MAX
380        );
381        assert_eq!(
382            FileTime::from_str_radix("+3w5e11264sgsf", 36).unwrap(),
383            FileTime::MAX
384        );
385        assert_eq!(
386            FileTime::from_str_radix("+3W5E11264SGSF", 36).unwrap(),
387            FileTime::MAX
388        );
389    }
390
391    #[test]
392    fn from_str_radix_with_invalid_digit_radix() {
393        assert_eq!(
394            FileTime::from_str_radix("2", 2)
395                .unwrap_err()
396                .source()
397                .unwrap()
398                .downcast_ref::<ParseIntError>()
399                .unwrap()
400                .kind(),
401            &IntErrorKind::InvalidDigit
402        );
403        assert_eq!(
404            FileTime::from_str_radix("8", 8)
405                .unwrap_err()
406                .source()
407                .unwrap()
408                .downcast_ref::<ParseIntError>()
409                .unwrap()
410                .kind(),
411            &IntErrorKind::InvalidDigit
412        );
413        assert_eq!(
414            FileTime::from_str_radix("a", 10)
415                .unwrap_err()
416                .source()
417                .unwrap()
418                .downcast_ref::<ParseIntError>()
419                .unwrap()
420                .kind(),
421            &IntErrorKind::InvalidDigit
422        );
423        assert_eq!(
424            FileTime::from_str_radix("A", 10)
425                .unwrap_err()
426                .source()
427                .unwrap()
428                .downcast_ref::<ParseIntError>()
429                .unwrap()
430                .kind(),
431            &IntErrorKind::InvalidDigit
432        );
433        assert_eq!(
434            FileTime::from_str_radix("g", 16)
435                .unwrap_err()
436                .source()
437                .unwrap()
438                .downcast_ref::<ParseIntError>()
439                .unwrap()
440                .kind(),
441            &IntErrorKind::InvalidDigit
442        );
443        assert_eq!(
444            FileTime::from_str_radix("G", 16)
445                .unwrap_err()
446                .source()
447                .unwrap()
448                .downcast_ref::<ParseIntError>()
449                .unwrap()
450                .kind(),
451            &IntErrorKind::InvalidDigit
452        );
453    }
454
455    #[test]
456    fn from_str_radix_when_empty() {
457        assert_eq!(
458            FileTime::from_str_radix("", 16)
459                .unwrap_err()
460                .source()
461                .unwrap()
462                .downcast_ref::<ParseIntError>()
463                .unwrap()
464                .kind(),
465            &IntErrorKind::Empty
466        );
467    }
468
469    #[test]
470    fn from_str_radix_with_invalid_digit() {
471        assert_eq!(
472            FileTime::from_str_radix("Z", 16)
473                .unwrap_err()
474                .source()
475                .unwrap()
476                .downcast_ref::<ParseIntError>()
477                .unwrap()
478                .kind(),
479            &IntErrorKind::InvalidDigit
480        );
481        assert_eq!(
482            FileTime::from_str_radix("_", 16)
483                .unwrap_err()
484                .source()
485                .unwrap()
486                .downcast_ref::<ParseIntError>()
487                .unwrap()
488                .kind(),
489            &IntErrorKind::InvalidDigit
490        );
491        assert_eq!(
492            FileTime::from_str_radix("-1", 16)
493                .unwrap_err()
494                .source()
495                .unwrap()
496                .downcast_ref::<ParseIntError>()
497                .unwrap()
498                .kind(),
499            &IntErrorKind::InvalidDigit
500        );
501        assert_eq!(
502            FileTime::from_str_radix("+", 16)
503                .unwrap_err()
504                .source()
505                .unwrap()
506                .downcast_ref::<ParseIntError>()
507                .unwrap()
508                .kind(),
509            &IntErrorKind::InvalidDigit
510        );
511        assert_eq!(
512            FileTime::from_str_radix("-", 16)
513                .unwrap_err()
514                .source()
515                .unwrap()
516                .downcast_ref::<ParseIntError>()
517                .unwrap()
518                .kind(),
519            &IntErrorKind::InvalidDigit
520        );
521        assert_eq!(
522            FileTime::from_str_radix(" 0", 16)
523                .unwrap_err()
524                .source()
525                .unwrap()
526                .downcast_ref::<ParseIntError>()
527                .unwrap()
528                .kind(),
529            &IntErrorKind::InvalidDigit
530        );
531        assert_eq!(
532            FileTime::from_str_radix("0 ", 16)
533                .unwrap_err()
534                .source()
535                .unwrap()
536                .downcast_ref::<ParseIntError>()
537                .unwrap()
538                .kind(),
539            &IntErrorKind::InvalidDigit
540        );
541    }
542
543    #[test]
544    fn from_str_radix_when_positive_overflow() {
545        assert_eq!(
546            FileTime::from_str_radix(
547                "10000000000000000000000000000000000000000000000000000000000000000",
548                2
549            )
550            .unwrap_err()
551            .source()
552            .unwrap()
553            .downcast_ref::<ParseIntError>()
554            .unwrap()
555            .kind(),
556            &IntErrorKind::PosOverflow
557        );
558        assert_eq!(
559            FileTime::from_str_radix("2000000000000000000000", 8)
560                .unwrap_err()
561                .source()
562                .unwrap()
563                .downcast_ref::<ParseIntError>()
564                .unwrap()
565                .kind(),
566            &IntErrorKind::PosOverflow
567        );
568        assert_eq!(
569            FileTime::from_str_radix("18446744073709551616", 10)
570                .unwrap_err()
571                .source()
572                .unwrap()
573                .downcast_ref::<ParseIntError>()
574                .unwrap()
575                .kind(),
576            &IntErrorKind::PosOverflow
577        );
578        assert_eq!(
579            FileTime::from_str_radix("10000000000000000", 16)
580                .unwrap_err()
581                .source()
582                .unwrap()
583                .downcast_ref::<ParseIntError>()
584                .unwrap()
585                .kind(),
586            &IntErrorKind::PosOverflow
587        );
588        assert_eq!(
589            FileTime::from_str_radix("3w5e11264sgsg", 36)
590                .unwrap_err()
591                .source()
592                .unwrap()
593                .downcast_ref::<ParseIntError>()
594                .unwrap()
595                .kind(),
596            &IntErrorKind::PosOverflow
597        );
598        assert_eq!(
599            FileTime::from_str_radix("3W5E11264SGSG", 36)
600                .unwrap_err()
601                .source()
602                .unwrap()
603                .downcast_ref::<ParseIntError>()
604                .unwrap()
605                .kind(),
606            &IntErrorKind::PosOverflow
607        );
608    }
609
610    #[test]
611    #[should_panic]
612    fn from_str_radix_when_radix_is_less_than_2() {
613        let _ = FileTime::from_str_radix("0", 1);
614    }
615
616    #[test]
617    #[should_panic]
618    fn from_str_radix_when_radix_is_greater_than_36() {
619        let _ = FileTime::from_str_radix("0", 37);
620    }
621
622    #[test]
623    fn from_str() {
624        assert_eq!(FileTime::from_str("0").unwrap(), FileTime::NT_TIME_EPOCH);
625        assert_eq!(FileTime::from_str("+0").unwrap(), FileTime::NT_TIME_EPOCH);
626        assert_eq!(
627            FileTime::from_str("116444736000000000").unwrap(),
628            FileTime::UNIX_EPOCH
629        );
630        assert_eq!(
631            FileTime::from_str("+116444736000000000").unwrap(),
632            FileTime::UNIX_EPOCH
633        );
634        assert_eq!(
635            FileTime::from_str("9223372036854775807").unwrap(),
636            FileTime::SIGNED_MAX
637        );
638        assert_eq!(
639            FileTime::from_str("+9223372036854775807").unwrap(),
640            FileTime::SIGNED_MAX
641        );
642        assert_eq!(
643            FileTime::from_str("18446744073709551615").unwrap(),
644            FileTime::MAX
645        );
646        assert_eq!(
647            FileTime::from_str("+18446744073709551615").unwrap(),
648            FileTime::MAX
649        );
650    }
651
652    #[cfg(feature = "std")]
653    #[test_strategy::proptest]
654    fn from_str_roundtrip(#[strategy(r"\+?[0-9]{1,19}")] s: std::string::String) {
655        use proptest::prop_assert_eq;
656
657        let ft = s.parse().unwrap();
658        prop_assert_eq!(FileTime::from_str(&s).unwrap(), FileTime::new(ft));
659    }
660
661    #[test]
662    fn from_str_when_empty() {
663        assert_eq!(
664            FileTime::from_str("")
665                .unwrap_err()
666                .source()
667                .unwrap()
668                .downcast_ref::<ParseIntError>()
669                .unwrap()
670                .kind(),
671            &IntErrorKind::Empty
672        );
673    }
674
675    #[test]
676    fn from_str_with_invalid_digit() {
677        assert_eq!(
678            FileTime::from_str("a")
679                .unwrap_err()
680                .source()
681                .unwrap()
682                .downcast_ref::<ParseIntError>()
683                .unwrap()
684                .kind(),
685            &IntErrorKind::InvalidDigit
686        );
687        assert_eq!(
688            FileTime::from_str("_")
689                .unwrap_err()
690                .source()
691                .unwrap()
692                .downcast_ref::<ParseIntError>()
693                .unwrap()
694                .kind(),
695            &IntErrorKind::InvalidDigit
696        );
697        assert_eq!(
698            FileTime::from_str("-1")
699                .unwrap_err()
700                .source()
701                .unwrap()
702                .downcast_ref::<ParseIntError>()
703                .unwrap()
704                .kind(),
705            &IntErrorKind::InvalidDigit
706        );
707        assert_eq!(
708            FileTime::from_str("+")
709                .unwrap_err()
710                .source()
711                .unwrap()
712                .downcast_ref::<ParseIntError>()
713                .unwrap()
714                .kind(),
715            &IntErrorKind::InvalidDigit
716        );
717        assert_eq!(
718            FileTime::from_str("-")
719                .unwrap_err()
720                .source()
721                .unwrap()
722                .downcast_ref::<ParseIntError>()
723                .unwrap()
724                .kind(),
725            &IntErrorKind::InvalidDigit
726        );
727        assert_eq!(
728            FileTime::from_str(" 0")
729                .unwrap_err()
730                .source()
731                .unwrap()
732                .downcast_ref::<ParseIntError>()
733                .unwrap()
734                .kind(),
735            &IntErrorKind::InvalidDigit
736        );
737        assert_eq!(
738            FileTime::from_str("0 ")
739                .unwrap_err()
740                .source()
741                .unwrap()
742                .downcast_ref::<ParseIntError>()
743                .unwrap()
744                .kind(),
745            &IntErrorKind::InvalidDigit
746        );
747    }
748
749    #[cfg(feature = "std")]
750    #[test_strategy::proptest]
751    fn from_str_with_invalid_digit_roundtrip(
752        #[strategy(r"-[0-9]+|[^0-9]+")] s: std::string::String,
753    ) {
754        use proptest::prop_assert;
755
756        prop_assert!(FileTime::from_str(&s).is_err());
757    }
758
759    #[test]
760    fn from_str_when_positive_overflow() {
761        assert_eq!(
762            FileTime::from_str("18446744073709551616")
763                .unwrap_err()
764                .source()
765                .unwrap()
766                .downcast_ref::<ParseIntError>()
767                .unwrap()
768                .kind(),
769            &IntErrorKind::PosOverflow
770        );
771    }
772}