darkwing/server/services/sqlite_services/
chrome_time.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use rusqlite::{types::FromSql, ToSql};
use static_assertions::const_assert;
use time::OffsetDateTime;

const CHROME_DATE_OFFSET: i64 = 11_644_473_600_000_000;

#[allow(unused)]
const UNIX_TIMESTAMP_2000: i64 = 946_684_800;
#[allow(unused)]
const UNIX_TIMESTAMP_2026: i64 = 17_976_931_200;

const_assert!(i64::MAX > UNIX_TIMESTAMP_2026 * 1_000_000 + CHROME_DATE_OFFSET);

#[derive(Debug, Clone, PartialEq)]
pub struct ChromeTime {
  inner: OffsetDateTime,
}

impl ChromeTime {
  pub(super) fn from_unix_timestamp(value: i64) -> anyhow::Result<Self> {
    Ok(Self {
      inner: OffsetDateTime::from_unix_timestamp(value)?,
    })
  }
}

impl ToSql for ChromeTime {
  /// Chrome stores the expiration date as the number of 100-nanosecond
  /// intervals since January 1, 1601 (UTC).
  ///
  /// We need to convert this from seconds since Unix epoch (January 1, 1970).
  fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
    let time: i64 = self
      .inner
      .unix_timestamp()
      .checked_mul(1_000_000)
      .and_then(|result| result.checked_add(CHROME_DATE_OFFSET))
      .ok_or(rusqlite::Error::ToSqlConversionFailure(Box::from(
        "Failed to convert inner to Chrome format",
      )))?;
    Ok(rusqlite::types::ToSqlOutput::from(time))
  }
}

impl FromSql for ChromeTime {
  fn column_result(
    bytes: rusqlite::types::ValueRef<'_>,
  ) -> rusqlite::types::FromSqlResult<Self> {
    let time = bytes
      .as_i64()
      .map_err(|e| {
        rusqlite::types::FromSqlError::Other(Box::from(format!(
          "Failed to get i64 value: {}",
          e
        )))
      })?
      .checked_sub(CHROME_DATE_OFFSET)
      .ok_or(rusqlite::types::FromSqlError::Other(Box::from(
        "Failed to convert Chrome format to inner, checked_sub failed",
      )))?
      .checked_div(1_000_000)
      .ok_or(rusqlite::types::FromSqlError::Other(Box::from(
        "Failed to convert Chrome format to inner, checked_div failed",
      )))?;

    Self::from_unix_timestamp(time).map_err(|_| {
      rusqlite::types::FromSqlError::Other(Box::from(
        "Failed to convert Chrome format to inner",
      ))
    })
  }
}

#[cfg(test)]
mod tests {
  use super::*;
  use rusqlite::types::Value;
  use time::macros::datetime;

  #[test]
  fn test_chrome_time_conversion() -> anyhow::Result<()> {
    // Test case 1: Modern date (2024-03-14 15:00:00 UTC)
    let modern_date = datetime!(2024-03-14 15:00:00 UTC);
    let chrome_time = ChromeTime { inner: modern_date };

    // Convert to Chrome format
    let chrome_value = match chrome_time.to_sql()? {
      rusqlite::types::ToSqlOutput::Owned(Value::Integer(i)) => i,
      _ => panic!("Expected integer output"),
    };

    // Convert back from Chrome format
    let value_ref = rusqlite::types::ValueRef::Integer(chrome_value);
    let recovered_time = ChromeTime::column_result(value_ref)?;

    assert_eq!(recovered_time.inner, modern_date);

    // Test case 2: Date close to Unix epoch (1970-01-01 00:01:00 UTC)
    let near_epoch = datetime!(1970-01-01 00:01:00 UTC);
    let chrome_time = ChromeTime { inner: near_epoch };

    let chrome_value = match chrome_time.to_sql()? {
      rusqlite::types::ToSqlOutput::Owned(Value::Integer(i)) => i,
      _ => panic!("Expected integer output"),
    };

    let value_ref = rusqlite::types::ValueRef::Integer(chrome_value);
    let recovered_time = ChromeTime::column_result(value_ref)?;

    assert_eq!(recovered_time.inner, near_epoch);

    // Test case 3: Future date (2025-12-31 23:59:59 UTC)
    let future_date = datetime!(2025-12-31 23:59:59 UTC);
    let chrome_time = ChromeTime { inner: future_date };

    let chrome_value = match chrome_time.to_sql()? {
      rusqlite::types::ToSqlOutput::Owned(Value::Integer(i)) => i,
      _ => panic!("Expected integer output"),
    };

    let value_ref = rusqlite::types::ValueRef::Integer(chrome_value);
    let recovered_time = ChromeTime::column_result(value_ref)?;

    assert_eq!(recovered_time.inner, future_date);

    Ok(())
  }

  #[test]
  fn test_invalid_chrome_time() {
    // Test with a value that would overflow when converting
    let huge_value = i64::MAX;
    let value_ref = rusqlite::types::ValueRef::Integer(huge_value);
    let result = ChromeTime::column_result(value_ref);
    assert!(result.is_err());

    // Test with a negative value
    let negative_value = i64::MIN;
    let value_ref = rusqlite::types::ValueRef::Integer(negative_value);
    let result = ChromeTime::column_result(value_ref);
    assert!(result.is_err());
  }

  #[test]
  fn test_chrome_time_edge_cases() -> anyhow::Result<()> {
    // Test with earliest reasonable date (1970-01-01 00:00:00 UTC)
    let epoch_start = datetime!(1970-01-01 00:00:00 UTC);
    let chrome_time = ChromeTime { inner: epoch_start };

    let chrome_value = match chrome_time.to_sql()? {
      rusqlite::types::ToSqlOutput::Owned(Value::Integer(i)) => i,
      _ => panic!("Expected integer output"),
    };

    let value_ref = rusqlite::types::ValueRef::Integer(chrome_value);
    let recovered_time = ChromeTime::column_result(value_ref)?;

    assert_eq!(recovered_time.inner, epoch_start);

    Ok(())
  }
}