mesh_protobuf/
time.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Time types for mesh protobuf encoding.
5
6use crate::DecodeError;
7use crate::MessageDecode;
8use crate::MessageEncode;
9use crate::inplace::InplaceOption;
10use crate::inplace_some;
11use crate::protobuf::MessageReader;
12use crate::protobuf::MessageSizer;
13use crate::protobuf::MessageWriter;
14use crate::protofile::DescribeField;
15use crate::protofile::FieldType;
16use crate::protofile::MessageDescription;
17use crate::table::DescribeTable;
18use crate::table::TableEncoder;
19use core::time::Duration;
20use mesh_protobuf::Protobuf;
21use thiserror::Error;
22
23const NANOS_PER_SEC: u32 = 1_000_000_000;
24
25impl DescribeTable for Timestamp {
26    const DESCRIPTION: MessageDescription<'static> = MessageDescription::External {
27        name: "google.protobuf.Timestamp",
28        import_path: "google/protobuf/timestamp.proto",
29    };
30}
31
32/// A timestamp representing a point in UTC time with nanosecond resolution.
33#[derive(Debug, Protobuf, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
34pub struct Timestamp {
35    /// The number of seconds of UTC time since the Unix epoch.
36    #[mesh(1, encoding = "mesh_protobuf::encoding::VarintField")]
37    pub seconds: i64,
38    /// Non-negative fractions of a second at nanosecond resolution.
39    #[mesh(2, encoding = "mesh_protobuf::encoding::VarintField")]
40    pub nanos: i32,
41}
42
43#[cfg(feature = "std")]
44impl From<std::time::SystemTime> for Timestamp {
45    fn from(value: std::time::SystemTime) -> Self {
46        match value.duration_since(std::time::UNIX_EPOCH) {
47            Ok(since_epoch) => Self {
48                seconds: since_epoch.as_secs() as i64,
49                nanos: since_epoch.subsec_nanos() as i32,
50            },
51            Err(err) => {
52                let since_epoch = err.duration();
53                if since_epoch.subsec_nanos() == 0 {
54                    Self {
55                        seconds: -(since_epoch.as_secs() as i64),
56                        nanos: 0,
57                    }
58                } else {
59                    Self {
60                        seconds: -(since_epoch.as_secs() as i64) - 1,
61                        nanos: (1_000_000_000 - since_epoch.subsec_nanos()) as i32,
62                    }
63                }
64            }
65        }
66    }
67}
68
69#[derive(Debug, Error)]
70#[error("timestamp out of range for system time")]
71pub struct TimestampOutOfRange;
72
73#[cfg(feature = "std")]
74impl TryFrom<Timestamp> for std::time::SystemTime {
75    type Error = TimestampOutOfRange;
76
77    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
78        if value.nanos < 0 || value.nanos >= NANOS_PER_SEC as i32 {
79            return Err(TimestampOutOfRange);
80        }
81        if value.seconds >= 0 {
82            std::time::SystemTime::UNIX_EPOCH
83                .checked_add(Duration::new(value.seconds as u64, value.nanos as u32))
84        } else {
85            let secs = value.seconds.checked_neg().ok_or(TimestampOutOfRange)? as u64;
86            if value.nanos == 0 {
87                std::time::SystemTime::UNIX_EPOCH.checked_sub(Duration::new(secs, 0))
88            } else {
89                std::time::SystemTime::UNIX_EPOCH
90                    .checked_sub(Duration::new(secs - 1, NANOS_PER_SEC - value.nanos as u32))
91            }
92        }
93        .ok_or(TimestampOutOfRange)
94    }
95}
96
97/// Protobuf-compatible encoding for [`Duration`].
98pub struct DurationEncoding;
99
100impl DescribeField<Duration> for DurationEncoding {
101    const FIELD_TYPE: FieldType<'static> = FieldType::builtin("google.protobuf.Duration");
102}
103
104impl<R> MessageEncode<Duration, R> for DurationEncoding {
105    fn write_message(item: Duration, writer: MessageWriter<'_, '_, R>) {
106        TableEncoder::write_message((item.as_secs(), item.subsec_nanos()), writer);
107    }
108
109    fn compute_message_size(item: &mut Duration, sizer: MessageSizer<'_>) {
110        <TableEncoder as MessageEncode<_, R>>::compute_message_size(
111            &mut (item.as_secs(), item.subsec_nanos()),
112            sizer,
113        );
114    }
115}
116
117impl<R> MessageDecode<'_, Duration, R> for DurationEncoding {
118    fn read_message(
119        item: &mut InplaceOption<'_, Duration>,
120        reader: MessageReader<'_, '_, R>,
121    ) -> crate::Result<()> {
122        let duration = item.take().unwrap_or_default();
123        let message = (duration.as_secs(), duration.subsec_nanos());
124        inplace_some!(message);
125        TableEncoder::read_message(&mut message, reader)?;
126        let (secs, nanos) = message.take().unwrap();
127        if (secs as i64) < 0 || nanos >= NANOS_PER_SEC {
128            return Err(DecodeError::DurationRange.into());
129        }
130        item.set(Duration::new(secs, nanos));
131        Ok(())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137
138    #[cfg(feature = "std")]
139    #[test]
140    fn test_timestamp_system_time() {
141        use super::Timestamp;
142        use std::time::SystemTime;
143
144        let check = |st: SystemTime| {
145            let st2 = SystemTime::try_from(Timestamp::from(st)).unwrap();
146            assert_eq!(st, st2);
147        };
148
149        check(SystemTime::now());
150        check(SystemTime::now() + core::time::Duration::from_secs(1));
151        check(SystemTime::now() - core::time::Duration::from_secs(1));
152        check(SystemTime::UNIX_EPOCH - core::time::Duration::from_nanos(1_500_000_000));
153        check(SystemTime::UNIX_EPOCH + core::time::Duration::from_nanos(1_500_000_000));
154
155        assert_eq!(
156            Timestamp::from(
157                SystemTime::UNIX_EPOCH - core::time::Duration::from_nanos(1_500_000_000)
158            ),
159            Timestamp {
160                seconds: -2,
161                nanos: 500_000_000,
162            }
163        );
164    }
165}