darkwing/server/services/sqlite_services/
chrome_time.rsuse 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 {
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<()> {
let modern_date = datetime!(2024-03-14 15:00:00 UTC);
let chrome_time = ChromeTime { inner: modern_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, modern_date);
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);
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() {
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());
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<()> {
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(())
}
}