use std::fs::OpenOptions;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
pub const MARKER_MAGIC: &[u8; 8] = b"ZCCSYMv1";
pub const MARKER_SIZE: usize = 128;
const OFFSET_SHA: usize = 0;
const OFFSET_VERSION: usize = 40;
const OFFSET_TRIPLE: usize = 56;
const OFFSET_TIMESTAMP: usize = 88;
const OFFSET_MAGIC: usize = 120;
const SHA_LEN: usize = 40;
const VERSION_LEN: usize = 16;
const TRIPLE_LEN: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReleaseMarker {
pub git_sha: String,
pub version: String,
pub triple: String,
pub build_timestamp: u64,
}
#[must_use]
pub fn read_marker_from_current_exe() -> Option<ReleaseMarker> {
let exe = std::env::current_exe().ok()?;
read_marker_from_path(&exe)
}
#[must_use]
pub fn read_marker_from_path(path: &Path) -> Option<ReleaseMarker> {
let mut file = std::fs::File::open(path).ok()?;
let len = file.metadata().ok()?.len();
if len < MARKER_SIZE as u64 {
return None;
}
file.seek(SeekFrom::End(-(MARKER_SIZE as i64))).ok()?;
let mut buf = [0u8; MARKER_SIZE];
file.read_exact(&mut buf).ok()?;
decode_footer(&buf)
}
pub fn write_marker_to_binary(path: &Path, marker: &ReleaseMarker) -> std::io::Result<()> {
let footer = encode_footer(marker)?;
let mut file = OpenOptions::new().append(true).open(path)?;
file.write_all(&footer)?;
file.flush()?;
Ok(())
}
fn decode_footer(buf: &[u8; MARKER_SIZE]) -> Option<ReleaseMarker> {
if &buf[OFFSET_MAGIC..OFFSET_MAGIC + 8] != MARKER_MAGIC {
return None;
}
let git_sha = decode_nul_padded(&buf[OFFSET_SHA..OFFSET_SHA + SHA_LEN])?;
let version = decode_nul_padded(&buf[OFFSET_VERSION..OFFSET_VERSION + VERSION_LEN])?;
let triple = decode_nul_padded(&buf[OFFSET_TRIPLE..OFFSET_TRIPLE + TRIPLE_LEN])?;
let mut ts_bytes = [0u8; 8];
ts_bytes.copy_from_slice(&buf[OFFSET_TIMESTAMP..OFFSET_TIMESTAMP + 8]);
let build_timestamp = u64::from_le_bytes(ts_bytes);
Some(ReleaseMarker {
git_sha,
version,
triple,
build_timestamp,
})
}
fn encode_footer(marker: &ReleaseMarker) -> std::io::Result<[u8; MARKER_SIZE]> {
let mut out = [0u8; MARKER_SIZE];
write_nul_padded(
&mut out[OFFSET_SHA..OFFSET_SHA + SHA_LEN],
&marker.git_sha,
"git_sha",
)?;
write_nul_padded(
&mut out[OFFSET_VERSION..OFFSET_VERSION + VERSION_LEN],
&marker.version,
"version",
)?;
write_nul_padded(
&mut out[OFFSET_TRIPLE..OFFSET_TRIPLE + TRIPLE_LEN],
&marker.triple,
"triple",
)?;
out[OFFSET_TIMESTAMP..OFFSET_TIMESTAMP + 8]
.copy_from_slice(&marker.build_timestamp.to_le_bytes());
out[OFFSET_MAGIC..OFFSET_MAGIC + 8].copy_from_slice(MARKER_MAGIC);
Ok(out)
}
fn decode_nul_padded(slice: &[u8]) -> Option<String> {
let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
std::str::from_utf8(&slice[..end]).ok().map(str::to_owned)
}
fn write_nul_padded(dest: &mut [u8], value: &str, field: &str) -> std::io::Result<()> {
let bytes = value.as_bytes();
if bytes.len() > dest.len() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"{field}: {} bytes exceeds slot size {}",
bytes.len(),
dest.len()
),
));
}
dest[..bytes.len()].copy_from_slice(bytes);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_marker() -> ReleaseMarker {
ReleaseMarker {
git_sha: "032432c000000000000000000000000000000000".to_string(),
version: "1.7.2".to_string(),
triple: "x86_64-pc-windows-msvc".to_string(),
build_timestamp: 1_700_000_000,
}
}
#[test]
fn roundtrip_encode_decode() {
let m = sample_marker();
let footer = encode_footer(&m).unwrap();
assert_eq!(footer.len(), MARKER_SIZE);
assert_eq!(&footer[OFFSET_MAGIC..OFFSET_MAGIC + 8], MARKER_MAGIC);
let parsed = decode_footer(&footer).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn triple_too_long_errors() {
let mut m = sample_marker();
m.triple = "x86_64-some-extremely-long-triple-that-cannot-fit".to_string();
assert!(encode_footer(&m).is_err());
}
#[test]
fn missing_magic_returns_none() {
let mut buf = [0u8; MARKER_SIZE];
buf[OFFSET_VERSION..OFFSET_VERSION + 5].copy_from_slice(b"1.7.2");
assert!(decode_footer(&buf).is_none());
}
#[test]
fn write_marker_appends_to_file() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"existing binary contents goes here")
.unwrap();
let path = tmp.into_temp_path();
let m = sample_marker();
write_marker_to_binary(&path, &m).unwrap();
let parsed = read_marker_from_path(&path).unwrap();
assert_eq!(parsed, m);
}
#[test]
fn short_file_returns_none() {
let mut tmp = tempfile::NamedTempFile::new().unwrap();
tmp.write_all(b"tiny").unwrap();
let path = tmp.into_temp_path();
assert!(read_marker_from_path(&path).is_none());
}
}