use crate::{ChronoError, Format, PackedCodec, PosixNs};
use forensicnomicon::temporal_formats::{Encoding, PackedLayout, SQLSERVER_EPOCH_NS, TIME_FORMATS};
use std::sync::LazyLock;
fn decode_fat_dos(value: i64) -> Result<PosixNs, ChronoError> {
let packed = u32::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "FAT/DOS packed value (not a u32)",
value: i128::from(value),
})?;
let date = (packed >> 16) as u16;
let time = (packed & 0xFFFF) as u16;
let year = 1980 + ((date >> 9) & 0x7F) as i16;
let month = ((date >> 5) & 0x0F) as i8;
let day = (date & 0x1F) as i8;
let hour = ((time >> 11) & 0x1F) as i8;
let minute = ((time >> 5) & 0x3F) as i8;
let second = ((time & 0x1F) * 2) as i8;
let dt = jiff::civil::DateTime::new(year, month, day, hour, minute, second, 0)
.map_err(|e| ChronoError::Render(e.to_string()))?;
let ts = dt
.to_zoned(jiff::tz::TimeZone::UTC)
.map_err(|e| ChronoError::Render(e.to_string()))?
.timestamp();
Ok(PosixNs(ts.as_nanosecond()))
}
fn packed_civil(
year: i16,
month: i8,
day: i8,
hour: i8,
minute: i8,
second: i8,
) -> Result<PosixNs, ChronoError> {
let dt = jiff::civil::DateTime::new(year, month, day, hour, minute, second, 0)
.map_err(|e| ChronoError::Render(e.to_string()))?;
let ts = dt
.to_zoned(jiff::tz::TimeZone::UTC)
.map_err(|e| ChronoError::Render(e.to_string()))?
.timestamp();
Ok(PosixNs(ts.as_nanosecond()))
}
fn civil_fields(instant: PosixNs) -> Result<(i16, i8, i8, i8, i8, i8), ChronoError> {
let ts = jiff::Timestamp::from_nanosecond(instant.0)
.map_err(|e| ChronoError::Render(e.to_string()))?;
let dt = jiff::tz::Offset::UTC.to_datetime(ts);
Ok((
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
))
}
fn encode_fat_dos(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(1980..=2107).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "FAT/DOS year (1980-2107)",
value: i128::from(year),
});
}
let date = (u32::from((year - 1980) as u16) << 9)
| (u32::from(month as u8) << 5)
| u32::from(day as u8);
let time = (u32::from(hour as u8) << 11)
| (u32::from(minute as u8) << 5)
| u32::from((second / 2) as u8);
Ok(i64::from((date << 16) | time))
}
fn decode_exfat(value: i64) -> Result<PosixNs, ChronoError> {
let p = u32::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "exFAT packed value (not a u32)",
value: i128::from(value),
})?;
packed_civil(
1980 + ((p >> 25) & 0x7F) as i16,
((p >> 21) & 0x0F) as i8,
((p >> 16) & 0x1F) as i8,
((p >> 11) & 0x1F) as i8,
((p >> 5) & 0x3F) as i8,
((p & 0x1F) * 2) as i8,
)
}
fn encode_exfat(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(1980..=2107).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "exFAT year (1980-2107)",
value: i128::from(year),
});
}
let p = (u32::from((year - 1980) as u16) << 25)
| (u32::from(month as u8) << 21)
| (u32::from(day as u8) << 16)
| (u32::from(hour as u8) << 11)
| (u32::from(minute as u8) << 5)
| u32::from((second / 2) as u8);
Ok(i64::from(p))
}
fn decode_dttm(value: i64) -> Result<PosixNs, ChronoError> {
let p = u32::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "DTTM packed value (not a u32)",
value: i128::from(value),
})?;
packed_civil(
1900 + ((p >> 20) & 0x1FF) as i16,
((p >> 16) & 0x0F) as i8,
((p >> 11) & 0x1F) as i8,
((p >> 6) & 0x1F) as i8,
(p & 0x3F) as i8,
0,
)
}
fn encode_dttm(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, _second) = civil_fields(instant)?;
if !(1900..=2411).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "DTTM year (1900-2411)",
value: i128::from(year),
});
}
let p = (u32::from((year - 1900) as u16) << 20)
| (u32::from(month as u8) << 16)
| (u32::from(day as u8) << 11)
| (u32::from(hour as u8) << 6)
| u32::from(minute as u8);
Ok(i64::from(p))
}
fn decode_bitdate(value: i64) -> Result<PosixNs, ChronoError> {
let p = u32::try_from(value)
.map_err(|_| ChronoError::OutOfRange {
what: "BitDate packed value (not a u32)",
value: i128::from(value),
})?
.swap_bytes();
packed_civil(
((p >> 20) & 0xFFF) as i16,
((p >> 16) & 0x0F) as i8,
((p >> 11) & 0x1F) as i8,
((p >> 6) & 0x1F) as i8,
(p & 0x3F) as i8,
0,
)
}
fn encode_bitdate(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, _second) = civil_fields(instant)?;
if !(0..=4095).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "BitDate year (0-4095)",
value: i128::from(year),
});
}
let p = (u32::from(year as u16) << 20)
| (u32::from(month as u8) << 16)
| (u32::from(day as u8) << 11)
| (u32::from(hour as u8) << 6)
| u32::from(minute as u8);
Ok(i64::from(p.swap_bytes()))
}
fn decode_bitdec(value: i64) -> Result<PosixNs, ChronoError> {
if value < 0 {
return Err(ChronoError::OutOfRange {
what: "Bitwise Decimal value (negative)",
value: i128::from(value),
});
}
packed_civil(
i16::try_from(value >> 20).map_err(|_| ChronoError::OutOfRange {
what: "Bitwise Decimal year",
value: i128::from(value),
})?,
((value >> 16) & 15) as i8,
((value >> 11) & 31) as i8,
((value >> 6) & 31) as i8,
(value & 63) as i8,
0,
)
}
fn encode_bitdec(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, _second) = civil_fields(instant)?;
if year < 0 {
return Err(ChronoError::OutOfRange {
what: "Bitwise Decimal year (non-negative)",
value: i128::from(year),
});
}
Ok((i64::from(year) << 20)
| (i64::from(month) << 16)
| (i64::from(day) << 11)
| (i64::from(hour) << 6)
| i64::from(minute))
}
fn decode_bcd(value: i64) -> Result<PosixNs, ChronoError> {
if !(0..1_000_000_000_000).contains(&value) {
return Err(ChronoError::OutOfRange {
what: "BCD value (not 12 decimal digits)",
value: i128::from(value),
});
}
let s = format!("{value:012}");
let pair = |i: usize| -> i8 { s[i..i + 2].parse().unwrap_or(-1) };
packed_civil(
2000 + i16::from(pair(0)),
pair(2),
pair(4),
pair(6),
pair(8),
pair(10),
)
}
fn encode_bcd(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(2000..=2099).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "BCD year (2000-2099)",
value: i128::from(year),
});
}
let s = format!(
"{:02}{:02}{:02}{:02}{:02}{:02}",
year - 2000,
month,
day,
hour,
minute,
second
);
s.parse().map_err(|_| ChronoError::OutOfRange {
what: "BCD packed value",
value: i128::from(year),
})
}
fn decode_moto(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value)
.ok()
.filter(|v| *v <= 0xFFFF_FFFF_FFFF)
.ok_or(ChronoError::OutOfRange {
what: "Motorola (not a 6-byte value)",
value: i128::from(value),
})?;
let byte = |sh: u32| ((v >> sh) & 0xFF) as i8;
packed_civil(
1970 + i16::from(((v >> 40) & 0xFF) as u8),
byte(32),
byte(24),
byte(16),
byte(8),
byte(0),
)
}
fn decode_symantec(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value)
.ok()
.filter(|v| *v <= 0xFFFF_FFFF_FFFF)
.ok_or(ChronoError::OutOfRange {
what: "Symantec (not a 6-byte value)",
value: i128::from(value),
})?;
let byte = |sh: u32| ((v >> sh) & 0xFF) as i8;
packed_civil(
1970 + i16::from(((v >> 40) & 0xFF) as u8),
byte(32).wrapping_add(1),
byte(24),
byte(16),
byte(8),
byte(0),
)
}
fn decode_dvr(value: i64) -> Result<PosixNs, ChronoError> {
let p = u32::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "DVR packed value (not a u32)",
value: i128::from(value),
})?;
packed_civil(
2000 + ((p >> 26) & 0x3F) as i16,
((p >> 22) & 0x0F) as i8,
((p >> 17) & 0x1F) as i8,
((p >> 12) & 0x1F) as i8,
((p >> 6) & 0x3F) as i8,
(p & 0x3F) as i8,
)
}
fn decode_ns40(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value)
.ok()
.filter(|v| *v <= 0xFF_FFFF_FFFF_FFFF)
.ok_or(ChronoError::OutOfRange {
what: "Nokia S40 (not a 7-byte value)",
value: i128::from(value),
})?;
let byte = |sh: u32| ((v >> sh) & 0xFF) as i8;
let yr = i16::try_from((v >> 40) & 0xFFFF).map_err(|_| ChronoError::OutOfRange {
what: "Nokia S40 year",
value: i128::from(value),
})?;
packed_civil(yr, byte(32), byte(24), byte(16), byte(8), byte(0))
}
fn decode_ns40le(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value)
.ok()
.filter(|v| *v <= 0xFF_FFFF_FFFF_FFFF)
.ok_or(ChronoError::OutOfRange {
what: "Nokia S40 LE (not a 7-byte value)",
value: i128::from(value),
})?;
let byte = |sh: u32| ((v >> sh) & 0xFF) as i8;
let yr = i16::try_from((((v >> 40) & 0xFFFF) as u16).swap_bytes()).map_err(|_| {
ChronoError::OutOfRange {
what: "Nokia S40 LE year",
value: i128::from(value),
}
})?;
packed_civil(yr, byte(32), byte(24), byte(16), byte(8), byte(0))
}
fn encode_moto(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
let yb = u8::try_from(year - 1970).map_err(|_| ChronoError::OutOfRange {
what: "Motorola year (1970-2225)",
value: i128::from(year),
})?;
let v = (u64::from(yb) << 40)
| (u64::from(month as u8) << 32)
| (u64::from(day as u8) << 24)
| (u64::from(hour as u8) << 16)
| (u64::from(minute as u8) << 8)
| u64::from(second as u8);
Ok(v as i64)
}
fn encode_symantec(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
let yb = u8::try_from(year - 1970).map_err(|_| ChronoError::OutOfRange {
what: "Symantec year (1970-2225)",
value: i128::from(year),
})?;
let v = (u64::from(yb) << 40)
| (u64::from((month - 1) as u8) << 32)
| (u64::from(day as u8) << 24)
| (u64::from(hour as u8) << 16)
| (u64::from(minute as u8) << 8)
| u64::from(second as u8);
Ok(v as i64)
}
fn encode_dvr(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(2000..=2063).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "DVR year (2000-2063)",
value: i128::from(year),
});
}
let p = (u32::from((year - 2000) as u8) << 26)
| (u32::from(month as u8) << 22)
| (u32::from(day as u8) << 17)
| (u32::from(hour as u8) << 12)
| (u32::from(minute as u8) << 6)
| u32::from(second as u8);
Ok(i64::from(p))
}
fn encode_ns40(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
let yr = u16::try_from(year).map_err(|_| ChronoError::OutOfRange {
what: "Nokia S40 year (0-65535)",
value: i128::from(year),
})?;
let v = (u64::from(yr) << 40)
| (u64::from(month as u8) << 32)
| (u64::from(day as u8) << 24)
| (u64::from(hour as u8) << 16)
| (u64::from(minute as u8) << 8)
| u64::from(second as u8);
Ok(v as i64)
}
fn encode_ns40le(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
let yr = u16::try_from(year).map_err(|_| ChronoError::OutOfRange {
what: "Nokia S40 LE year (0-65535)",
value: i128::from(year),
})?;
let v = (u64::from(yr.swap_bytes()) << 40)
| (u64::from(month as u8) << 32)
| (u64::from(day as u8) << 24)
| (u64::from(hour as u8) << 16)
| (u64::from(minute as u8) << 8)
| u64::from(second as u8);
Ok(v as i64)
}
fn decode_logtime(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "JET LogTime (negative)",
value: i128::from(value),
})?;
let byte = |sh: u32| ((v >> sh) & 0xFF) as i8;
let yr = 1900 + ((v >> 16) & 0xFF) as i16;
packed_civil(yr, byte(24), byte(32), byte(40), byte(48), byte(56))
}
fn encode_logtime(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(1900..=2155).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "JET LogTime year (1900-2155)",
value: i128::from(year),
});
}
let yb = u64::from((year - 1900) as u16);
let v = (yb << 16)
| (u64::from(month as u8) << 24)
| (u64::from(day as u8) << 32)
| (u64::from(hour as u8) << 40)
| (u64::from(minute as u8) << 48)
| (u64::from(second as u8) << 56);
Ok(v as i64)
}
fn semi_pair(low: u8, high: u8) -> i8 {
i8::try_from(low * 10 + high).unwrap_or(-1)
}
fn decode_semioctet(value: i64) -> Result<PosixNs, ChronoError> {
if !(0..1_000_000_000_000).contains(&value) {
return Err(ChronoError::OutOfRange {
what: "Semi-Octet (not 12 decimal digits)",
value: i128::from(value),
});
}
let s = format!("{value:012}");
let b = s.as_bytes();
let pair = |i: usize| semi_pair(b[i + 1] - b'0', b[i] - b'0');
packed_civil(
2000 + i16::from(pair(0)),
pair(2),
pair(4),
pair(6),
pair(8),
pair(10),
)
}
fn semi_swap(field: i8) -> u64 {
let f = field as u64;
(f % 10) * 10 + (f / 10)
}
fn encode_semioctet(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(2000..=2099).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "Semi-Octet year (2000-2099)",
value: i128::from(year),
});
}
let yy = (year - 2000) as i8;
let v = semi_swap(yy) * 10_000_000_000
+ semi_swap(month) * 100_000_000
+ semi_swap(day) * 1_000_000
+ semi_swap(hour) * 10_000
+ semi_swap(minute) * 100
+ semi_swap(second);
Ok(v as i64)
}
fn decode_gsm(value: i64) -> Result<PosixNs, ChronoError> {
let v = u64::try_from(value)
.ok()
.filter(|v| *v <= 0xFF_FFFF_FFFF_FFFF)
.ok_or(ChronoError::OutOfRange {
what: "GSM (not a 7-byte value)",
value: i128::from(value),
})?;
let semi = |sh: u32| {
let byte = ((v >> sh) & 0xFF) as u8;
semi_pair(byte & 0x0F, byte >> 4)
};
packed_civil(
2000 + i16::from(semi(48)),
semi(40),
semi(32),
semi(24),
semi(16),
semi(8),
)
}
fn gsm_byte(field: i8) -> u64 {
let f = field as u64;
((f % 10) << 4) | (f / 10)
}
fn encode_gsm(instant: PosixNs) -> Result<i64, ChronoError> {
let (year, month, day, hour, minute, second) = civil_fields(instant)?;
if !(2000..=2099).contains(&year) {
return Err(ChronoError::OutOfRange {
what: "GSM year (2000-2099)",
value: i128::from(year),
});
}
let yy = (year - 2000) as i8;
let v = (gsm_byte(yy) << 48)
| (gsm_byte(month) << 40)
| (gsm_byte(day) << 32)
| (gsm_byte(hour) << 24)
| (gsm_byte(minute) << 16)
| (gsm_byte(second) << 8);
Ok(v as i64)
}
fn decode_nokiale(value: i64) -> Result<PosixNs, ChronoError> {
let p = i64::from(
u32::try_from(value)
.map_err(|_| ChronoError::OutOfRange {
what: "Nokia LE (not a u32)",
value: i128::from(value),
})?
.swap_bytes(),
);
let unix = p - 0xFFFF_FFFF + 2_524_608_000; Ok(PosixNs(i128::from(unix) * 1_000_000_000))
}
fn encode_nokiale(instant: PosixNs) -> Result<i64, ChronoError> {
let unix = instant.0.div_euclid(1_000_000_000);
let p = unix + 0xFFFF_FFFF - 2_524_608_000;
let p = u32::try_from(p).map_err(|_| ChronoError::OutOfRange {
what: "Nokia LE seconds-before-2050 (out of u32 range)",
value: unix,
})?;
Ok(i64::from(p.swap_bytes()))
}
fn decode_sqlserver(value: i64) -> Result<PosixNs, ChronoError> {
let v = value as u64;
let days = i128::from((v >> 32) as i32);
let ticks = v & 0xFFFF_FFFF;
if ticks >= 25_920_000 {
return Err(ChronoError::OutOfRange {
what: "SQL Server datetime ticks (>= one day)",
value: i128::from(ticks),
});
}
let ns = SQLSERVER_EPOCH_NS
+ days * 86_400 * 1_000_000_000
+ (i128::from(ticks) * 1_000_000_000) / 300;
Ok(PosixNs(ns))
}
fn encode_sqlserver(instant: PosixNs) -> Result<i64, ChronoError> {
const NS_PER_DAY: i128 = 86_400 * 1_000_000_000;
let rel = instant
.0
.checked_sub(SQLSERVER_EPOCH_NS)
.ok_or(ChronoError::OutOfRange {
what: "SQL Server ns since 1900 (overflow)",
value: instant.0,
})?; let days = rel.div_euclid(NS_PER_DAY);
let rem_ns = rel.rem_euclid(NS_PER_DAY);
let mut ticks = (rem_ns * 300 + 500_000_000) / 1_000_000_000;
let mut days = days;
if ticks >= 25_920_000 {
ticks -= 25_920_000;
days += 1;
}
let days = i32::try_from(days).map_err(|_| ChronoError::OutOfRange {
what: "SQL Server days-since-1900 (out of i32 range)",
value: days,
})?;
Ok((i64::from(days) << 32) | ticks as i64)
}
fn packed_codec(layout: PackedLayout) -> PackedCodec {
match layout {
PackedLayout::FatDos => PackedCodec {
decode: decode_fat_dos,
encode: Some(encode_fat_dos),
},
PackedLayout::ExFat => PackedCodec {
decode: decode_exfat,
encode: Some(encode_exfat),
},
PackedLayout::Dttm => PackedCodec {
decode: decode_dttm,
encode: Some(encode_dttm),
},
PackedLayout::BitDate => PackedCodec {
decode: decode_bitdate,
encode: Some(encode_bitdate),
},
PackedLayout::BitDec => PackedCodec {
decode: decode_bitdec,
encode: Some(encode_bitdec),
},
PackedLayout::Bcd => PackedCodec {
decode: decode_bcd,
encode: Some(encode_bcd),
},
PackedLayout::Moto => PackedCodec {
decode: decode_moto,
encode: Some(encode_moto),
},
PackedLayout::Symantec => PackedCodec {
decode: decode_symantec,
encode: Some(encode_symantec),
},
PackedLayout::Dvr => PackedCodec {
decode: decode_dvr,
encode: Some(encode_dvr),
},
PackedLayout::Ns40 => PackedCodec {
decode: decode_ns40,
encode: Some(encode_ns40),
},
PackedLayout::Ns40Le => PackedCodec {
decode: decode_ns40le,
encode: Some(encode_ns40le),
},
PackedLayout::LogTime => PackedCodec {
decode: decode_logtime,
encode: Some(encode_logtime),
},
PackedLayout::SemiOctet => PackedCodec {
decode: decode_semioctet,
encode: Some(encode_semioctet),
},
PackedLayout::Gsm => PackedCodec {
decode: decode_gsm,
encode: Some(encode_gsm),
},
PackedLayout::NokiaLe => PackedCodec {
decode: decode_nokiale,
encode: Some(encode_nokiale),
},
PackedLayout::SqlServer => PackedCodec {
decode: decode_sqlserver,
encode: Some(encode_sqlserver),
},
}
}
pub static FORMATS: LazyLock<Vec<Format>> = LazyLock::new(|| {
TIME_FORMATS
.iter()
.map(|meta| Format {
meta,
packed: match meta.encoding {
Encoding::Packed(layout) => Some(packed_codec(layout)),
Encoding::LinearInt { .. }
| Encoding::LinearFloat { .. }
| Encoding::Embedded { .. } => None,
},
})
.collect()
});