use sha2::{Digest, Sha256};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, PoisonError, RwLock};
pub trait JitStore: Send + Sync + 'static {
fn get(&self, key: &str) -> io::Result<Option<Vec<u8>>>;
fn put(&self, key: &str, value: &[u8]) -> io::Result<()>;
fn contains(&self, key: &str) -> io::Result<bool> {
Ok(self.get(key)?.is_some())
}
fn delete(&self, key: &str) -> io::Result<()>;
fn clear(&self) -> io::Result<()>;
}
#[derive(Debug)]
pub struct FileSystemJitStore {
root: PathBuf,
capacity_bytes: u64,
high_watermark: f64,
low_watermark: f64,
}
pub struct FileSystemJitStoreBuilder {
root: PathBuf,
capacity_bytes: u64,
high_watermark: f64,
low_watermark: f64,
}
pub const DEFAULT_CAPACITY_BYTES: u64 = 2 << 30;
pub const EVICTION_LOCK_FILE_NAME: &str = ".eviction.lock";
const STALE_TEMP_AGE: std::time::Duration = std::time::Duration::from_secs(3600);
impl FileSystemJitStoreBuilder {
pub fn capacity_bytes(mut self, capacity_bytes: u64) -> Self {
self.capacity_bytes = capacity_bytes;
self
}
pub fn eviction_watermarks(mut self, high: f64, low: f64) -> Self {
self.high_watermark = high;
self.low_watermark = low;
self
}
pub fn open(self) -> io::Result<FileSystemJitStore> {
if !(self.low_watermark > 0.0
&& self.low_watermark <= self.high_watermark
&& self.high_watermark.is_finite())
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"eviction watermarks need 0 < low <= high, got high={} low={}",
self.high_watermark, self.low_watermark
),
));
}
std::fs::create_dir_all(&self.root)?;
Ok(FileSystemJitStore {
root: self.root,
capacity_bytes: self.capacity_bytes,
high_watermark: self.high_watermark,
low_watermark: self.low_watermark,
})
}
}
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
fn eviction_draw(nonce: &uuid::Uuid) -> u64 {
let b = nonce.as_bytes();
let lo = u64::from_le_bytes(b[..8].try_into().expect("uuid is 16 bytes"));
let hi = u64::from_le_bytes(b[8..].try_into().expect("uuid is 16 bytes"));
lo ^ hi
}
impl FileSystemJitStore {
pub fn new(dir: impl Into<PathBuf>) -> io::Result<Self> {
Self::builder(dir).open()
}
pub fn builder(dir: impl Into<PathBuf>) -> FileSystemJitStoreBuilder {
FileSystemJitStoreBuilder {
root: dir.into(),
capacity_bytes: DEFAULT_CAPACITY_BYTES,
high_watermark: 1.0,
low_watermark: 0.8,
}
}
pub fn default_location() -> io::Result<Self> {
let root = default_cache_dir()?.join("cutile").join("kernels");
#[cfg(unix)]
prepare_default_cache_root(&root)?;
Self::new(root)
}
pub fn root(&self) -> &Path {
&self.root
}
fn entry_path(&self, key: &str) -> io::Result<PathBuf> {
let valid = key.len() == 64
&& key
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
if !valid {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("jit store keys are 64 lowercase hex chars, got {key:?}"),
));
}
Ok(self.root.join(&key[..2]).join(format!("{key}.cubin")))
}
fn evict(&self) {
let lock_file = match std::fs::File::create(self.root.join(EVICTION_LOCK_FILE_NAME)) {
Ok(f) => f,
Err(e) => {
STATS.io_errors.fetch_add(1, Ordering::Relaxed);
cache_log(format_args!("evict: cannot open lock file: {e}"));
return;
}
};
match lock_file.try_lock() {
Ok(()) => {}
Err(std::fs::TryLockError::WouldBlock) => return,
Err(std::fs::TryLockError::Error(e)) => {
STATS.io_errors.fetch_add(1, Ordering::Relaxed);
cache_log(format_args!("evict: lock failed: {e}"));
return;
}
}
let now = std::time::SystemTime::now();
let mut entries: Vec<(PathBuf, u64, std::time::SystemTime)> = Vec::new();
let mut total: u64 = 0;
let Ok(shards) = std::fs::read_dir(&self.root) else {
STATS.io_errors.fetch_add(1, Ordering::Relaxed);
return;
};
for shard in shards.flatten() {
let is_shard = shard.file_name().len() == 2
&& shard
.file_name()
.to_str()
.is_some_and(|s| s.bytes().all(|b| b.is_ascii_hexdigit()));
if !is_shard || !shard.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let Ok(files) = std::fs::read_dir(shard.path()) else {
continue;
};
for file in files.flatten() {
let Ok(meta) = file.metadata() else { continue };
let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
let name = file.file_name();
let name = name.to_string_lossy();
if name.contains(".tmp.") {
if now
.duration_since(mtime)
.is_ok_and(|age| age > STALE_TEMP_AGE)
{
let _ = std::fs::remove_file(file.path());
}
continue;
}
if !name.ends_with(".cubin") {
continue;
}
total += meta.len();
entries.push((file.path(), meta.len(), mtime));
}
}
let capacity = self.capacity_bytes as f64;
if total as f64 <= capacity * self.high_watermark {
return;
}
let target = capacity * self.low_watermark;
entries.sort_by_key(|(_, _, mtime)| *mtime);
let mut deleted = 0u64;
for (path, len, _) in entries {
if total as f64 <= target {
break;
}
match std::fs::remove_file(&path) {
Ok(()) => {
total -= len;
deleted += 1;
}
Err(_) => {}
}
}
cache_log(format_args!(
"evict: deleted {deleted} entries, {total} bytes remain (capacity {})",
self.capacity_bytes
));
}
}
pub(crate) fn cache_log(msg: std::fmt::Arguments<'_>) {
use std::sync::OnceLock;
static ENABLED: OnceLock<bool> = OnceLock::new();
if *ENABLED.get_or_init(|| crate::cuda_tile_runtime_utils::env_flag_enabled("CUTILE_JIT_LOG")) {
eprintln!("[cutile::jit] {msg}");
}
}
impl JitStore for FileSystemJitStore {
fn get(&self, key: &str) -> io::Result<Option<Vec<u8>>> {
let path = self.entry_path(key)?;
match std::fs::read(&path) {
Ok(bytes) => {
if self.capacity_bytes > 0 {
let times =
std::fs::FileTimes::new().set_modified(std::time::SystemTime::now());
let _ = std::fs::File::options()
.write(true)
.open(&path)
.and_then(|f| f.set_times(times));
}
Ok(Some(bytes))
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
fn put(&self, key: &str, value: &[u8]) -> io::Result<()> {
let final_path = self.entry_path(key)?;
if self.capacity_bytes > 0
&& value.len() as f64 > self.capacity_bytes as f64 * self.low_watermark
{
cache_log(format_args!(
"not caching {key}: entry is {} bytes, above the low watermark \
({:.0} bytes); storing it would evict the entire cache",
value.len(),
self.capacity_bytes as f64 * self.low_watermark,
));
return Ok(());
}
let shard = final_path
.parent()
.expect("entry path always has a shard parent");
std::fs::create_dir_all(shard)?;
let nonce = uuid::Uuid::new_v4();
let temp_path = shard.join(format!(
"{key}.tmp.{}.{}.{nonce}",
std::process::id(),
TEMP_COUNTER.fetch_add(1, Ordering::Relaxed),
));
std::fs::write(&temp_path, value).inspect_err(|_| {
let _ = std::fs::remove_file(&temp_path);
})?;
std::fs::rename(&temp_path, &final_path).inspect_err(|_| {
let _ = std::fs::remove_file(&temp_path);
})?;
if self.capacity_bytes > 0 {
let threshold = u128::from((self.capacity_bytes / 16).max(1));
let len = value.len() as u128;
let draw = u128::from(eviction_draw(&nonce));
if draw * threshold < len << 64 {
self.evict();
}
}
Ok(())
}
fn contains(&self, key: &str) -> io::Result<bool> {
match std::fs::metadata(self.entry_path(key)?) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
fn delete(&self, key: &str) -> io::Result<()> {
match std::fs::remove_file(self.entry_path(key)?) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
fn clear(&self) -> io::Result<()> {
for entry in std::fs::read_dir(&self.root)? {
let entry = entry?;
let name = entry.file_name();
let is_shard = name.len() == 2
&& name
.to_str()
.is_some_and(|s| s.bytes().all(|b| b.is_ascii_hexdigit()));
if is_shard && entry.file_type()?.is_dir() {
std::fs::remove_dir_all(entry.path())?;
}
}
Ok(())
}
}
#[cfg(unix)]
fn prepare_default_cache_root(root: &Path) -> io::Result<()> {
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
let parent = root.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("default JIT cache path has no parent: {}", root.display()),
)
})?;
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(parent)?;
match std::fs::DirBuilder::new().mode(0o700).create(root) {
Ok(()) => {
std::fs::set_permissions(root, std::fs::Permissions::from_mode(0o700))?;
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
let metadata = std::fs::symlink_metadata(root)?;
if !metadata.file_type().is_dir() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"refusing to use default JIT cache path {} because it is not a directory; delete it and retry",
root.display()
),
));
}
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"refusing to use pre-existing JIT cache directory {} with mode {:04o} because it grants permissions to group or others; delete it and retry",
root.display(),
mode & 0o777,
),
));
}
std::fs::set_permissions(root, std::fs::Permissions::from_mode(0o700))
}
Err(e) => Err(e),
}
}
fn default_cache_dir() -> io::Result<PathBuf> {
#[cfg(unix)]
{
if let Some(dir) = std::env::var_os("XDG_CACHE_HOME").filter(|v| !v.is_empty()) {
return Ok(PathBuf::from(dir));
}
if let Some(home) = std::env::var_os("HOME").filter(|v| !v.is_empty()) {
return Ok(PathBuf::from(home).join(".cache"));
}
}
#[cfg(windows)]
{
if let Some(dir) = std::env::var_os("LOCALAPPDATA").filter(|v| !v.is_empty()) {
return Ok(PathBuf::from(dir));
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
"no per-user cache directory: set XDG_CACHE_HOME or HOME (Unix) / \
LOCALAPPDATA (Windows), or open a FileSystemJitStore at an explicit path",
))
}
static STORE: RwLock<Option<Arc<dyn JitStore>>> = RwLock::new(None);
pub fn enable(store: Arc<dyn JitStore>) {
*STORE.write().unwrap_or_else(PoisonError::into_inner) = Some(store);
}
pub fn enable_default() -> io::Result<()> {
enable(Arc::new(FileSystemJitStore::default_location()?));
Ok(())
}
pub fn disable() {
*STORE.write().unwrap_or_else(PoisonError::into_inner) = None;
}
pub fn is_enabled() -> bool {
STORE
.read()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
pub(crate) fn installed_store() -> Option<Arc<dyn JitStore>> {
STORE.read().unwrap_or_else(PoisonError::into_inner).clone()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct JitCacheStats {
pub hits: u64,
pub misses: u64,
pub puts: u64,
pub bytes_written: u64,
pub io_errors: u64,
}
pub(crate) struct StatCounters {
pub hits: AtomicU64,
pub misses: AtomicU64,
pub puts: AtomicU64,
pub bytes_written: AtomicU64,
pub io_errors: AtomicU64,
}
pub(crate) static STATS: StatCounters = StatCounters {
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
puts: AtomicU64::new(0),
bytes_written: AtomicU64::new(0),
io_errors: AtomicU64::new(0),
};
pub fn stats() -> JitCacheStats {
JitCacheStats {
hits: STATS.hits.load(Ordering::Relaxed),
misses: STATS.misses.load(Ordering::Relaxed),
puts: STATS.puts.load(Ordering::Relaxed),
bytes_written: STATS.bytes_written.load(Ordering::Relaxed),
io_errors: STATS.io_errors.load(Ordering::Relaxed),
}
}
static BACKEND_COMPILES: AtomicU64 = AtomicU64::new(0);
pub(crate) fn record_backend_compile() {
BACKEND_COMPILES.fetch_add(1, Ordering::Relaxed);
}
pub fn jit_backend_compile_count() -> u64 {
BACKEND_COMPILES.load(Ordering::Relaxed)
}
pub fn jit_disk_hit_count() -> u64 {
STATS.hits.load(Ordering::Relaxed)
}
const DOMAIN: &[u8] = b"cutile-jit-cubin-v1\0";
pub fn l2_key(
bc: &[u8],
bc_version: cutile_ir::bytecode::BytecodeVersion,
gpu_name: &str,
opt_level: u8,
tileiras_fp: &str,
) -> String {
let mut h = Sha256::new();
h.update(DOMAIN);
put_field(&mut h, &[bc_version.major, bc_version.minor]);
put_field(&mut h, &bc_version.tag.to_le_bytes());
put_field(&mut h, bc);
put_field(&mut h, gpu_name.as_bytes());
put_field(&mut h, &[opt_level]);
put_field(&mut h, tileiras_fp.as_bytes());
hex(&h.finalize())
}
fn put_field(h: &mut Sha256, bytes: &[u8]) {
h.update((bytes.len() as u64).to_le_bytes());
h.update(bytes);
}
fn hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
const ENTRY_MAGIC: &[u8; 12] = b"CUTILECUBIN\0";
const ENTRY_FORMAT_VERSION: u16 = 1;
const ENTRY_HEADER_LEN: usize = 96;
pub struct EntryParams<'a> {
pub bc_sha256: [u8; 32],
pub gpu_name: &'a str,
pub opt_level: u8,
pub tileiras_fp: &'a str,
}
pub fn encode_entry(params: &EntryParams<'_>, cubin: &[u8]) -> Option<Vec<u8>> {
let gpu_len: u16 = params.gpu_name.len().try_into().ok()?;
let fp_len: u16 = params.tileiras_fp.len().try_into().ok()?;
let mut out = Vec::with_capacity(
ENTRY_HEADER_LEN + params.gpu_name.len() + params.tileiras_fp.len() + cubin.len(),
);
out.extend_from_slice(ENTRY_MAGIC);
out.extend_from_slice(&ENTRY_FORMAT_VERSION.to_le_bytes());
out.extend_from_slice(&[0u8; 2]); out.extend_from_slice(&Sha256::digest(cubin));
out.extend_from_slice(¶ms.bc_sha256);
out.extend_from_slice(&gpu_len.to_le_bytes());
out.extend_from_slice(&fp_len.to_le_bytes());
out.push(params.opt_level);
out.extend_from_slice(&[0u8; 3]); out.extend_from_slice(&(cubin.len() as u64).to_le_bytes());
debug_assert_eq!(out.len(), ENTRY_HEADER_LEN);
out.extend_from_slice(params.gpu_name.as_bytes());
out.extend_from_slice(params.tileiras_fp.as_bytes());
out.extend_from_slice(cubin);
Some(out)
}
pub fn decode_entry(bytes: &[u8], params: &EntryParams<'_>) -> Option<Vec<u8>> {
if bytes.len() < ENTRY_HEADER_LEN
|| &bytes[0..12] != ENTRY_MAGIC
|| u16::from_le_bytes(bytes[12..14].try_into().unwrap()) != ENTRY_FORMAT_VERSION
{
return None;
}
let payload_sha256: [u8; 32] = bytes[16..48].try_into().unwrap();
let bc_sha256: [u8; 32] = bytes[48..80].try_into().unwrap();
let gpu_len = u16::from_le_bytes(bytes[80..82].try_into().unwrap()) as usize;
let fp_len = u16::from_le_bytes(bytes[82..84].try_into().unwrap()) as usize;
let opt_level = bytes[84];
let payload_len = u64::from_le_bytes(bytes[88..96].try_into().unwrap());
let payload_len: usize = payload_len.try_into().ok()?;
let expected_total = ENTRY_HEADER_LEN
.checked_add(gpu_len)?
.checked_add(fp_len)?
.checked_add(payload_len)?;
if bytes.len() != expected_total {
return None;
}
let gpu_name = &bytes[ENTRY_HEADER_LEN..ENTRY_HEADER_LEN + gpu_len];
let fp = &bytes[ENTRY_HEADER_LEN + gpu_len..ENTRY_HEADER_LEN + gpu_len + fp_len];
let payload = &bytes[ENTRY_HEADER_LEN + gpu_len + fp_len..];
if bc_sha256 != params.bc_sha256
|| gpu_name != params.gpu_name.as_bytes()
|| opt_level != params.opt_level
|| fp != params.tileiras_fp.as_bytes()
|| <[u8; 32]>::from(Sha256::digest(payload)) != payload_sha256
{
return None;
}
Some(payload.to_vec())
}
#[cfg(test)]
mod tests {
use super::*;
use cutile_ir::bytecode::BytecodeVersion;
#[cfg(unix)]
struct DefaultRootTestDir(PathBuf);
#[cfg(unix)]
impl DefaultRootTestDir {
fn new(label: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"cutile_default_cache_{label}_{}_{}",
std::process::id(),
uuid::Uuid::new_v4(),
));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn cache_root(&self) -> PathBuf {
self.0.join("cutile").join("kernels")
}
}
#[cfg(unix)]
impl Drop for DefaultRootTestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(unix)]
fn unix_mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[cfg(unix)]
#[test]
fn default_cache_root_is_created_private() {
let dir = DefaultRootTestDir::new("new");
let root = dir.cache_root();
prepare_default_cache_root(&root).unwrap();
assert_eq!(unix_mode(&root), 0o700);
}
#[cfg(unix)]
#[test]
fn default_cache_root_retains_private_existing_directory() {
use std::os::unix::fs::PermissionsExt;
let dir = DefaultRootTestDir::new("retain_private");
let root = dir.cache_root();
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("trusted-entry"), b"keep").unwrap();
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o700)).unwrap();
prepare_default_cache_root(&root).unwrap();
assert_eq!(unix_mode(&root), 0o700);
assert_eq!(std::fs::read(root.join("trusted-entry")).unwrap(), b"keep");
}
#[cfg(unix)]
#[test]
fn default_cache_root_rejects_existing_non_private_directory() {
use std::os::unix::fs::PermissionsExt;
for mode in [0o755, 0o750, 0o770, 0o702] {
let dir = DefaultRootTestDir::new(&format!("reject_{mode:o}"));
let root = dir.cache_root();
std::fs::create_dir_all(&root).unwrap();
std::fs::write(root.join("untrusted-entry"), b"do not trust").unwrap();
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(mode)).unwrap();
let err = prepare_default_cache_root(&root).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert!(err.to_string().contains("delete it and retry"));
assert_eq!(unix_mode(&root), mode);
assert!(root.join("untrusted-entry").exists());
}
}
#[test]
fn eviction_draw_has_no_fixed_bits() {
let mut seen_one = 0u64;
let mut seen_zero = 0u64;
for _ in 0..1024 {
let draw = eviction_draw(&uuid::Uuid::new_v4());
seen_one |= draw;
seen_zero |= !draw;
}
assert_eq!(seen_one, u64::MAX, "bits never 1: {:#066b}", !seen_one);
assert_eq!(
seen_zero,
u64::MAX,
"bits never 0 (bit 54 stuck was the uuid version-nibble bug): {:#066b}",
!seen_zero
);
}
const V: BytecodeVersion = BytecodeVersion {
major: 13,
minor: 2,
tag: 0,
};
fn params<'a>(bc: &[u8]) -> EntryParams<'a> {
EntryParams {
bc_sha256: Sha256::digest(bc).into(),
gpu_name: "sm_90",
opt_level: 3,
tileiras_fp: "release 13.3, V13.3.36",
}
}
#[test]
fn key_is_deterministic_and_field_sensitive() {
let base = l2_key(b"bc", V, "sm_90", 3, "fp");
assert_eq!(base, l2_key(b"bc", V, "sm_90", 3, "fp"));
assert_eq!(base.len(), 64);
assert!(base.bytes().all(|b| b.is_ascii_hexdigit()));
assert_ne!(base, l2_key(b"bc2", V, "sm_90", 3, "fp"));
assert_ne!(base, l2_key(b"bc", V, "sm_80", 3, "fp"));
assert_ne!(base, l2_key(b"bc", V, "sm_90", 0, "fp"));
assert_ne!(base, l2_key(b"bc", V, "sm_90", 3, "fp2"));
let v2 = BytecodeVersion {
major: 13,
minor: 3,
tag: 0,
};
assert_ne!(base, l2_key(b"bc", v2, "sm_90", 3, "fp"));
}
#[test]
fn key_length_prefix_blocks_field_boundary_shifts() {
let a = l2_key(b"bc", V, "sm_9", 3, "0fp");
let b = l2_key(b"bc", V, "sm_90", 3, "fp");
assert_ne!(a, b);
let c = l2_key(b"bcX", V, "sm_90", 3, "fp");
let d = l2_key(b"bc", V, "Xsm_90", 3, "fp");
assert_ne!(c, d);
}
#[test]
fn entry_roundtrip() {
let bc = b"some bytecode";
let cubin = b"the compiled cubin".to_vec();
let p = params(bc);
let encoded = encode_entry(&p, &cubin).unwrap();
assert_eq!(decode_entry(&encoded, &p), Some(cubin));
}
#[test]
fn entry_rejects_wrong_bc_sha256() {
let p_write = params(b"bytecode A");
let encoded = encode_entry(&p_write, b"cubin").unwrap();
let p_read = params(b"bytecode B");
assert_eq!(decode_entry(&encoded, &p_read), None);
}
#[test]
fn entry_rejects_field_mismatches() {
let bc = b"bc";
let encoded = encode_entry(¶ms(bc), b"cubin").unwrap();
let mut p = params(bc);
p.gpu_name = "sm_80";
assert_eq!(decode_entry(&encoded, &p), None);
let mut p = params(bc);
p.opt_level = 0;
assert_eq!(decode_entry(&encoded, &p), None);
let mut p = params(bc);
p.tileiras_fp = "other fp";
assert_eq!(decode_entry(&encoded, &p), None);
}
#[test]
fn entry_rejects_corruption_truncation_and_trailing_junk() {
let bc = b"bc";
let p = params(bc);
let encoded = encode_entry(&p, b"a cubin payload").unwrap();
let mut corrupt = encoded.clone();
*corrupt.last_mut().unwrap() ^= 0xff;
assert_eq!(decode_entry(&corrupt, &p), None);
for len in [0, 11, ENTRY_HEADER_LEN - 1, encoded.len() - 1] {
assert_eq!(decode_entry(&encoded[..len], &p), None, "len {len}");
}
let mut long = encoded.clone();
long.push(0);
assert_eq!(decode_entry(&long, &p), None);
let mut bad_magic = encoded.clone();
bad_magic[0] ^= 0xff;
assert_eq!(decode_entry(&bad_magic, &p), None);
let mut bad_version = encoded;
bad_version[12] ^= 0xff;
assert_eq!(decode_entry(&bad_version, &p), None);
}
#[test]
fn entry_rejects_oversized_names() {
let p = EntryParams {
bc_sha256: [0; 32],
gpu_name: "sm_90",
opt_level: 3,
tileiras_fp: &"x".repeat(usize::from(u16::MAX) + 1),
};
assert_eq!(encode_entry(&p, b"cubin"), None);
}
}