use crate::crc::{CrcStatus, crc16};
use crate::error::DecodeError;
use crate::reader::SpdImage;
use crate::timing::{Picoseconds, data_rate_mt_s};
use core::fmt;
const OFF_XMP_MAGIC: usize = 640;
const XMP_MAGIC: [u8; 2] = [0x0C, 0x4A];
const OFF_XMP_ENABLE: usize = 643;
const OFF_XMP_NAME1: usize = 654;
const BLOCK_LEN: usize = 64;
const OFF_XMP_PROFILE1: usize = 704;
const XMP_VPP: usize = 0;
const XMP_VDD: usize = 1;
const XMP_VDDQ: usize = 2;
const XMP_TCK: usize = 5;
const XMP_TAA: usize = 13;
const XMP_TRCD: usize = 15;
const XMP_TRP: usize = 17;
const XMP_TRAS: usize = 19;
const OFF_EXPO_MAGIC: usize = 832;
const EXPO_MAGIC: [u8; 4] = *b"EXPO";
const EXPO_BLOCK_LEN: usize = 128;
const OFF_EXPO_PROFILE1: usize = 842;
const EXPO_PROFILE_LEN: usize = 40;
const EXPO_VDD: usize = 0;
const EXPO_VDDQ: usize = 1;
const EXPO_VPP: usize = 2;
const EXPO_TCK: usize = 4;
const EXPO_TAA: usize = 6;
const EXPO_TRCD: usize = 8;
const EXPO_TRP: usize = 10;
const EXPO_TRAS: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct Millivolts(pub u16);
impl Millivolts {
#[must_use]
pub const fn millivolts(self) -> u16 {
self.0
}
}
impl fmt::Display for Millivolts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{:03} V", self.0 / 1000, self.0 % 1000)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RatedTimings {
pub cycle_time: Picoseconds,
pub data_rate_mt_s: u32,
pub cas_latency: u16,
pub taa: Picoseconds,
pub trcd: Picoseconds,
pub trp: Picoseconds,
pub tras: Picoseconds,
pub vdd: Millivolts,
pub vddq: Millivolts,
pub vpp: Millivolts,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct XmpProfile<'a> {
pub profile_number: u8,
pub name: Option<&'a str>,
pub rated: RatedTimings,
pub crc: CrcStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Xmp<'a> {
Absent,
Present {
header_crc: CrcStatus,
profile1: Option<XmpProfile<'a>>,
profile2: Option<XmpProfile<'a>>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ExpoProfile {
pub profile_number: u8,
pub rated: RatedTimings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Expo {
Absent,
Present {
block_crc: CrcStatus,
profile1: Option<ExpoProfile>,
profile2: Option<ExpoProfile>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct VendorProfiles<'a> {
pub xmp: Xmp<'a>,
pub expo: Expo,
}
pub fn decode_vendor_profiles(bytes: &[u8]) -> Result<VendorProfiles<'_>, DecodeError> {
Ok(VendorProfiles {
xmp: decode_xmp(bytes)?,
expo: decode_expo(bytes)?,
})
}
pub fn decode_xmp(bytes: &[u8]) -> Result<Xmp<'_>, DecodeError> {
let spd = SpdImage::new(bytes);
let magic = [spd.byte(OFF_XMP_MAGIC)?, spd.byte(OFF_XMP_MAGIC + 1)?];
if magic != XMP_MAGIC {
return Ok(Xmp::Absent);
}
let enable = spd.byte(OFF_XMP_ENABLE)?;
let header_crc = section_crc(&spd, OFF_XMP_MAGIC, BLOCK_LEN)?;
let profile1 = if enable & 0x01 != 0 {
Some(decode_xmp_profile(
&spd,
1,
OFF_XMP_PROFILE1,
OFF_XMP_NAME1,
)?)
} else {
None
};
let profile2 = if enable & 0x02 != 0 {
Some(decode_xmp_profile(
&spd,
2,
OFF_XMP_PROFILE1 + BLOCK_LEN,
OFF_XMP_NAME1 + 16,
)?)
} else {
None
};
Ok(Xmp::Present {
header_crc,
profile1,
profile2,
})
}
pub fn decode_expo(bytes: &[u8]) -> Result<Expo, DecodeError> {
let spd = SpdImage::new(bytes);
if spd.slice(OFF_EXPO_MAGIC, EXPO_MAGIC.len())? != EXPO_MAGIC {
return Ok(Expo::Absent);
}
let block_crc = section_crc(&spd, OFF_EXPO_MAGIC, EXPO_BLOCK_LEN)?;
let profile1 = decode_expo_profile(&spd, 1, OFF_EXPO_PROFILE1)?;
let profile2 = decode_expo_profile(&spd, 2, OFF_EXPO_PROFILE1 + EXPO_PROFILE_LEN)?;
Ok(Expo::Present {
block_crc,
profile1,
profile2,
})
}
fn decode_xmp_profile<'a>(
spd: &SpdImage<'a>,
number: u8,
base: usize,
name_off: usize,
) -> Result<XmpProfile<'a>, DecodeError> {
let rated = rated_timings(
spd,
Voltages {
vdd: spd.byte(base + XMP_VDD)?,
vddq: spd.byte(base + XMP_VDDQ)?,
vpp: spd.byte(base + XMP_VPP)?,
},
Offsets {
tck: base + XMP_TCK,
taa: base + XMP_TAA,
trcd: base + XMP_TRCD,
trp: base + XMP_TRP,
tras: base + XMP_TRAS,
},
)?;
let crc = section_crc(spd, base, BLOCK_LEN)?;
let name = read_name(spd, name_off)?;
Ok(XmpProfile {
profile_number: number,
name,
rated,
crc,
})
}
fn decode_expo_profile(
spd: &SpdImage,
number: u8,
base: usize,
) -> Result<Option<ExpoProfile>, DecodeError> {
if read_le_u16(spd, base + EXPO_TCK)? == 0 {
return Ok(None);
}
let rated = rated_timings(
spd,
Voltages {
vdd: spd.byte(base + EXPO_VDD)?,
vddq: spd.byte(base + EXPO_VDDQ)?,
vpp: spd.byte(base + EXPO_VPP)?,
},
Offsets {
tck: base + EXPO_TCK,
taa: base + EXPO_TAA,
trcd: base + EXPO_TRCD,
trp: base + EXPO_TRP,
tras: base + EXPO_TRAS,
},
)?;
Ok(Some(ExpoProfile {
profile_number: number,
rated,
}))
}
struct Voltages {
vdd: u8,
vddq: u8,
vpp: u8,
}
struct Offsets {
tck: usize,
taa: usize,
trcd: usize,
trp: usize,
tras: usize,
}
fn rated_timings(spd: &SpdImage, v: Voltages, o: Offsets) -> Result<RatedTimings, DecodeError> {
let cycle_time = ps(read_le_u16(spd, o.tck)?);
let taa = ps(read_le_u16(spd, o.taa)?);
Ok(RatedTimings {
cycle_time,
data_rate_mt_s: data_rate_mt_s(cycle_time),
cas_latency: rated_cas_latency(taa, cycle_time),
taa,
trcd: ps(read_le_u16(spd, o.trcd)?),
trp: ps(read_le_u16(spd, o.trp)?),
tras: ps(read_le_u16(spd, o.tras)?),
vdd: voltage(v.vdd),
vddq: voltage(v.vddq),
vpp: voltage(v.vpp),
})
}
fn section_crc(spd: &SpdImage, start: usize, block_len: usize) -> Result<CrcStatus, DecodeError> {
let covered = spd.slice(start, block_len - 2)?;
let computed = crc16(covered);
let stored = u16::from_le_bytes([
spd.byte(start + block_len - 2)?,
spd.byte(start + block_len - 1)?,
]);
Ok(CrcStatus {
computed,
stored,
matches: computed == stored,
})
}
fn ps(raw: u16) -> Picoseconds {
Picoseconds(u32::from(raw))
}
fn read_le_u16(spd: &SpdImage, offset: usize) -> Result<u16, DecodeError> {
Ok(u16::from_le_bytes([
spd.byte(offset)?,
spd.byte(offset + 1)?,
]))
}
fn voltage(byte: u8) -> Millivolts {
Millivolts(u16::from(byte >> 5) * 1000 + u16::from(byte & 0x1F) * 50)
}
fn rated_cas_latency(taa: Picoseconds, cycle_time: Picoseconds) -> u16 {
let tck = cycle_time.0;
if tck == 0 {
return 0;
}
((taa.0 + tck / 2) / tck) as u16
}
fn read_name<'a>(spd: &SpdImage<'a>, off: usize) -> Result<Option<&'a str>, DecodeError> {
let raw = spd.slice(off, 16)?;
let end = raw
.iter()
.rposition(|&b| b != b' ' && b != 0)
.map_or(0, |i| i + 1);
let trimmed = raw.get(..end).unwrap_or_default();
if trimmed.is_empty() || !trimmed.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
return Ok(None);
}
Ok(core::str::from_utf8(trimmed).ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn voltage_byte_to_millivolts() {
assert_eq!(voltage(0x25), Millivolts(1250));
assert_eq!(voltage(0x30), Millivolts(1800));
assert_eq!(voltage(0x00), Millivolts(0));
assert_eq!(voltage(0x20), Millivolts(1000)); assert_eq!(voltage(0x1F), Millivolts(1550)); }
#[test]
fn millivolts_display_is_three_decimal_volts() {
assert_eq!(render(Millivolts(1250)).as_bytes(), b"1.250 V");
assert_eq!(render(Millivolts(1800)).as_bytes(), b"1.800 V");
}
#[test]
fn rated_cas_latency_rounds_taa_in_cycles() {
assert_eq!(rated_cas_latency(Picoseconds(12654), Picoseconds(333)), 38);
assert_eq!(rated_cas_latency(Picoseconds(14280), Picoseconds(357)), 40);
assert_eq!(rated_cas_latency(Picoseconds(12654), Picoseconds(0)), 0);
}
#[test]
fn read_le_u16_low_byte_first() {
let spd = SpdImage::new(&[0x4D, 0x01]);
assert_eq!(read_le_u16(&spd, 0).unwrap(), 0x014D); }
#[test]
fn read_name_trims_padding_and_rejects_nonprintable() {
let mut img = [b' '; 16];
img[0] = b'T';
img[1] = b'G';
let spd = SpdImage::new(&img);
assert_eq!(read_name(&spd, 0).unwrap(), Some("TG"));
let blank = [b' '; 16];
assert_eq!(read_name(&SpdImage::new(&blank), 0).unwrap(), None);
let mut bad = [b'A'; 16];
bad[3] = 0x80;
assert_eq!(read_name(&SpdImage::new(&bad), 0).unwrap(), None);
assert!(matches!(
read_name(&SpdImage::new(&[b'A'; 8]), 0),
Err(DecodeError::Truncated { .. })
));
}
#[test]
fn absent_when_no_magic() {
let img = [0u8; 1024];
assert_eq!(decode_xmp(&img).unwrap(), Xmp::Absent);
assert_eq!(decode_expo(&img).unwrap(), Expo::Absent);
let both = decode_vendor_profiles(&img).unwrap();
assert_eq!(both.xmp, Xmp::Absent);
assert_eq!(both.expo, Expo::Absent);
}
#[test]
fn crafted_xmp_profile_decodes_rated_values() {
let mut img = [0u8; 832];
img[OFF_XMP_MAGIC] = XMP_MAGIC[0];
img[OFF_XMP_MAGIC + 1] = XMP_MAGIC[1];
img[OFF_XMP_ENABLE] = 0x01; write_xmp_profile(&mut img, OFF_XMP_PROFILE1, 333, 12654, 12654, 12654, 25974);
let Xmp::Present {
profile1, profile2, ..
} = decode_xmp(&img).unwrap()
else {
panic!("magic present, expected Xmp::Present");
};
assert!(profile2.is_none(), "profile 2 not enabled");
let p = profile1.expect("profile 1 enabled");
assert_eq!(p.profile_number, 1);
assert_eq!(p.rated.data_rate_mt_s, 6000);
assert_eq!(p.rated.cas_latency, 38);
assert_eq!(p.rated.trcd, Picoseconds(12654));
assert_eq!(p.rated.trp, Picoseconds(12654));
assert_eq!(p.rated.tras, Picoseconds(25974));
assert_eq!(p.rated.vdd, Millivolts(1250));
assert_eq!(p.rated.vpp, Millivolts(1800));
}
#[test]
fn crafted_expo_profile_decodes_rated_values() {
let mut img = [0u8; 960];
img[OFF_EXPO_MAGIC..OFF_EXPO_MAGIC + 4].copy_from_slice(&EXPO_MAGIC);
write_expo_profile(&mut img, OFF_EXPO_PROFILE1, 333, 12654, 12654, 12654, 25974);
let Expo::Present {
profile1, profile2, ..
} = decode_expo(&img).unwrap()
else {
panic!("magic present, expected Expo::Present");
};
assert!(
profile2.is_none(),
"profile 2 slot is zeroed -> unpopulated"
);
let p = profile1.expect("profile 1 populated");
assert_eq!(p.profile_number, 1);
assert_eq!(p.rated.data_rate_mt_s, 6000);
assert_eq!(p.rated.cas_latency, 38);
assert_eq!(p.rated.tras, Picoseconds(25974));
assert_eq!(p.rated.vdd, Millivolts(1250));
assert_eq!(p.rated.vpp, Millivolts(1800));
}
fn write_le_u16(img: &mut [u8], off: usize, value: u16) {
let [lo, hi] = value.to_le_bytes();
img[off] = lo;
img[off + 1] = hi;
}
fn write_xmp_profile(
img: &mut [u8],
base: usize,
tck: u16,
taa: u16,
trcd: u16,
trp: u16,
tras: u16,
) {
img[base + XMP_VPP] = 0x30; img[base + XMP_VDD] = 0x25; img[base + XMP_VDDQ] = 0x25; write_le_u16(img, base + XMP_TCK, tck);
write_le_u16(img, base + XMP_TAA, taa);
write_le_u16(img, base + XMP_TRCD, trcd);
write_le_u16(img, base + XMP_TRP, trp);
write_le_u16(img, base + XMP_TRAS, tras);
}
fn write_expo_profile(
img: &mut [u8],
base: usize,
tck: u16,
taa: u16,
trcd: u16,
trp: u16,
tras: u16,
) {
img[base + EXPO_VDD] = 0x25; img[base + EXPO_VDDQ] = 0x25; img[base + EXPO_VPP] = 0x30; write_le_u16(img, base + EXPO_TCK, tck);
write_le_u16(img, base + EXPO_TAA, taa);
write_le_u16(img, base + EXPO_TRCD, trcd);
write_le_u16(img, base + EXPO_TRP, trp);
write_le_u16(img, base + EXPO_TRAS, tras);
}
struct Sink {
data: [u8; 16],
len: usize,
}
impl Sink {
fn as_bytes(&self) -> &[u8] {
&self.data[..self.len]
}
}
impl fmt::Write for Sink {
fn write_str(&mut self, s: &str) -> fmt::Result {
for &b in s.as_bytes() {
self.data[self.len] = b;
self.len += 1;
}
Ok(())
}
}
fn render(value: impl fmt::Display) -> Sink {
use core::fmt::Write;
let mut sink = Sink {
data: [0; 16],
len: 0,
};
write!(sink, "{value}").unwrap();
sink
}
}