Skip to main content

structured_pades/
date.rs

1//! PDF calendar labels with explicit precision and original timezone spelling.
2
3use crate::error::{Error, ErrorKind, Syntax};
4use crate::model::admit;
5use crate::source::Location;
6use structured_asn1::memory::Work;
7use structured_pkix::{temporal::Instant, time::Calendar};
8
9/// Exact readable PDF date semantics, without a freshness or occurrence proof.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Date {
12    calendar: Calendar,
13    offset: i16,
14    components: u8,
15    explicit_zone: bool,
16    instant: Instant,
17}
18
19impl Date {
20    /// Original calendar components, with only schema-prescribed defaults.
21    #[must_use]
22    pub fn calendar(self) -> Calendar {
23        self.calendar
24    }
25    /// Original signed local-time offset in minutes.
26    #[must_use]
27    pub fn offset_minutes(self) -> i16 {
28        self.offset
29    }
30    /// Count of explicitly written year/month/day/hour/minute/second fields.
31    #[must_use]
32    pub fn components(self) -> u8 {
33        self.components
34    }
35    /// Whether the input explicitly wrote Z or a signed timezone.
36    #[must_use]
37    pub fn has_explicit_zone(self) -> bool {
38        self.explicit_zone
39    }
40    /// The resulting UTC label, without an authenticated clock assertion.
41    #[must_use]
42    pub fn instant(self) -> Instant {
43        self.instant
44    }
45
46    /// Reads ISO32000-1 ยง7.9.4, including D:, calendar bounds, ordered optional
47    /// fields, apostrophe placement and its prescribed GMT default. Numeric
48    /// defaults and calendar arithmetic are independent of application policy.
49    /// <https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf>
50    pub fn read(bytes: &[u8], location: Location, work: &impl Work) -> crate::Result<Self> {
51        admit(work, location, bytes.len() as u64 + 1)?;
52        let malformed = || Error {
53            kind: ErrorKind::Syntax(Syntax::String),
54            location,
55        };
56        if !bytes.starts_with(b"D:") || bytes.len() < 6 || bytes.len() > 22 {
57            return Err(malformed());
58        }
59        let mut at = 2;
60        let mut year = 0_i64;
61        for _ in 0..4 {
62            let byte = bytes[at];
63            if !byte.is_ascii_digit() {
64                return Err(malformed());
65            }
66            year = year * 10 + i64::from(byte - b'0');
67            at += 1;
68        }
69        let mut values = [1, 1, 0, 0, 0];
70        let mut components = 1_u8;
71        for value in &mut values {
72            if at == bytes.len() || !bytes[at].is_ascii_digit() {
73                break;
74            }
75            if at + 2 > bytes.len() || !bytes[at + 1].is_ascii_digit() {
76                return Err(malformed());
77            }
78            *value = (bytes[at] - b'0') * 10 + bytes[at + 1] - b'0';
79            components += 1;
80            at += 2;
81        }
82        let explicit_zone = at != bytes.len();
83        let mut offset = 0_i16;
84        if explicit_zone {
85            if components != 6 {
86                return Err(malformed());
87            }
88            let sign = bytes[at];
89            at += 1;
90            match sign {
91                b'Z' => {}
92                b'+' | b'-' => {
93                    let mut hours = 0_u8;
94                    let mut minutes = 0_u8;
95                    if at != bytes.len() {
96                        hours = pair(bytes, &mut at).ok_or_else(malformed)?;
97                        if at != bytes.len() {
98                            if bytes[at] != b'\'' {
99                                return Err(malformed());
100                            }
101                            at += 1;
102                            if at != bytes.len() {
103                                minutes = pair(bytes, &mut at).ok_or_else(malformed)?;
104                            }
105                        }
106                    }
107                    if hours > 23 || minutes > 59 {
108                        return Err(malformed());
109                    }
110                    offset = i16::from(hours) * 60 + i16::from(minutes);
111                    if sign == b'-' {
112                        offset = -offset;
113                    }
114                }
115                _ => return Err(malformed()),
116            }
117        }
118        if at != bytes.len() || values[4] > 59 {
119            return Err(malformed());
120        }
121        let calendar = Calendar {
122            year,
123            month: values[0],
124            day: values[1],
125            hour: values[2],
126            minute: values[3],
127            second: values[4],
128            nanos: 0,
129        };
130        let instant = calendar
131            .instant_at_minutes(offset, work)
132            .map_err(|cause| Error {
133                kind: ErrorKind::Resource(cause),
134                location,
135            })?
136            .ok_or_else(malformed)?;
137        Ok(Self {
138            calendar,
139            offset,
140            components,
141            explicit_zone,
142            instant,
143        })
144    }
145}
146
147fn pair(bytes: &[u8], at: &mut usize) -> Option<u8> {
148    if bytes.len() - *at < 2 {
149        return None;
150    }
151    let first = bytes[*at];
152    let second = bytes[*at + 1];
153    if !first.is_ascii_digit() || !second.is_ascii_digit() {
154        return None;
155    }
156    *at += 2;
157    Some((first - b'0') * 10 + second - b'0')
158}