use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderLayout {
pub version_hash: usize,
pub source_hash: usize,
pub flag_hash: usize,
pub read_only_checksum: Option<usize>,
pub payload_length: usize,
pub checksum: usize,
pub header_size: usize,
#[serde(default)]
pub num_reservations: Option<usize>,
#[serde(default)]
pub num_stub_keys: Option<usize>,
}
impl Default for HeaderLayout {
fn default() -> Self {
HeaderLayout {
version_hash: 4,
source_hash: 8,
flag_hash: 12,
read_only_checksum: None,
payload_length: 16,
checksum: 20,
header_size: 24,
num_reservations: None,
num_stub_keys: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Header {
pub raw_size: usize,
pub decompressed_size: usize,
pub brotli: bool,
pub magic: u32,
pub version_hash: u32,
pub source_hash: u32,
pub flag_hash: u32,
pub read_only_checksum: Option<u32>,
pub payload_length: u32,
pub checksum: u32,
pub layout: HeaderLayout,
pub payload_offset: usize,
}
impl Header {
pub fn parse(data: &[u8]) -> Result<Header, String> {
Self::parse_with(data, &HeaderLayout::default())
}
pub fn parse_with(data: &[u8], layout: &HeaderLayout) -> Result<Header, String> {
let brotli = !is_code_cache(data);
let raw_size = data.len();
let buf: Vec<u8>;
let data: &[u8] = if brotli {
buf = brotli_decompress(data)?;
&buf
} else {
data
};
let decompressed_size = data.len();
let need = layout.header_size;
if data.len() < need {
return Err(format!("file too small for a {need}-byte code cache header: {} bytes", data.len()));
}
let u32_at = |off: usize| -> Result<u32, String> {
let b = data
.get(off..off + 4)
.ok_or_else(|| format!("header truncated at offset {off}"))?;
Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
};
let magic = u32_at(0)?;
if (magic & 0xFFFF_0000) != 0xC0DE_0000 {
return Err(format!(
"bad magic {magic:#010x}: not a V8 code cache (expected 0xC0DE????)"
));
}
let read_only_checksum = layout
.read_only_checksum
.map(u32_at)
.transpose()?;
let payload_offset = layout.header_size;
Ok(Header {
raw_size,
decompressed_size,
brotli,
magic,
version_hash: u32_at(layout.version_hash)?,
source_hash: u32_at(layout.source_hash)?,
flag_hash: u32_at(layout.flag_hash)?,
read_only_checksum,
payload_length: u32_at(layout.payload_length)?,
checksum: u32_at(layout.checksum)?,
layout: layout.clone(),
payload_offset,
})
}
pub fn source_length(&self) -> u32 {
self.source_hash & 0x7FFF_FFFF
}
pub fn is_module(&self) -> bool {
self.source_hash >> 31 == 1
}
pub fn external_ref_table_size(&self) -> u32 {
self.magic & 0xFFFF
}
pub fn payload<'a>(&self, data: &'a [u8]) -> &'a [u8] {
let start = self.payload_offset;
let end = if self.payload_length > 0 && start + self.payload_length as usize <= data.len() {
start + self.payload_length as usize
} else {
data.len()
};
data.get(start..end).unwrap_or(&[])
}
}
pub fn is_code_cache(data: &[u8]) -> bool {
data.len() >= 4
&& u32::from_le_bytes([data[0], data[1], data[2], data[3]]) & 0xFFFF_0000 == 0xC0DE_0000
}
pub fn maybe_brotli_decompress(raw: Vec<u8>) -> Vec<u8> {
if is_code_cache(&raw) {
return raw;
}
match brotli_decompress(&raw) {
Ok(v) => v,
Err(_) => raw,
}
}
fn brotli_decompress(data: &[u8]) -> Result<Vec<u8>, String> {
let mut out = Vec::with_capacity(data.len() * 2);
let mut reader = brotli::Decompressor::new(data, 4096);
std::io::Read::read_to_end(&mut reader, &mut out)
.map_err(|e| format!("brotli decompress failed: {e}"))?;
if out.is_empty() {
return Err("brotli decompress produced no data".into());
}
Ok(out)
}
impl Header {
pub fn render_text(&self, file: &Path, ident: &crate::tables::Identified) -> String {
use std::fmt::Write as _;
let mut s = String::new();
let ro = self
.read_only_checksum
.map(|v| format!("{v:#010x}"))
.unwrap_or_else(|| "(absent)".into());
let _ = writeln!(s, "file: {}", file.display());
let _ = writeln!(
s,
"size: {} bytes (raw) / {} bytes (decompressed)",
self.raw_size, self.decompressed_size
);
let _ = writeln!(s, "brotli: {}", if self.brotli { "yes" } else { "no" });
let _ = writeln!(
s,
"magic: {:#010x} (external refs: {:#x})",
self.magic,
self.external_ref_table_size()
);
let _ = writeln!(s, "version_hash: {:#010x}", self.version_hash);
let _ = writeln!(
s,
"source_hash: {:#010x} (source length: {} bytes, module: {})",
self.source_hash,
self.source_length(),
if self.is_module() { "yes" } else { "no" }
);
let _ = writeln!(s, "flag_hash: {:#010x}", self.flag_hash);
let _ = writeln!(s, "ro_checksum: {ro}");
let _ = writeln!(s, "payload: {} bytes at offset {}", self.payload_length, self.payload_offset);
let _ = writeln!(s, "checksum: {:#010x}", self.checksum);
let _ = write!(s, "{}", ident.render_text());
s
}
pub fn render_json(&self, file: &Path, ident: &crate::tables::Identified) -> String {
#[derive(Serialize)]
struct InfoOut<'a> {
file: String,
size_raw: usize,
size_decompressed: usize,
brotli: bool,
magic: u32,
external_ref_table_size: u32,
version_hash: u32,
source_hash: u32,
source_length: u32,
is_module: bool,
flag_hash: u32,
read_only_checksum: Option<u32>,
payload_length: u32,
checksum: u32,
node: &'a str,
v8: &'a str,
identification: &'a str,
supported: bool,
}
let out = InfoOut {
file: file.display().to_string(),
size_raw: self.raw_size,
size_decompressed: self.decompressed_size,
brotli: self.brotli,
magic: self.magic,
external_ref_table_size: self.external_ref_table_size(),
version_hash: self.version_hash,
source_hash: self.source_hash,
source_length: self.source_length(),
is_module: self.is_module(),
flag_hash: self.flag_hash,
read_only_checksum: self.read_only_checksum,
payload_length: self.payload_length,
checksum: self.checksum,
node: ident.node,
v8: ident.v8,
identification: ident.confidence,
supported: ident.supported(),
};
serde_json::to_string_pretty(&out).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_dummy(vh: u32, sh: u32, fh: u32, pl: u32, ck: u32) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&0xC0DE_0000u32.to_le_bytes());
v.extend_from_slice(&vh.to_le_bytes());
v.extend_from_slice(&sh.to_le_bytes());
v.extend_from_slice(&fh.to_le_bytes());
v.extend_from_slice(&pl.to_le_bytes());
v.extend_from_slice(&ck.to_le_bytes());
v
}
#[test]
fn parses_v11_header() {
let data = build_dummy(0x11223344, 999, 0x55, 7, 0);
let h = Header::parse(&data).unwrap();
assert_eq!(h.version_hash, 0x11223344);
assert_eq!(h.source_length(), 999);
assert!(!h.is_module());
assert_eq!(h.payload_offset, 24);
assert_eq!(h.read_only_checksum, None);
assert!(!h.brotli);
}
#[test]
fn parses_v13_header() {
let layout = HeaderLayout {
version_hash: 4,
source_hash: 8,
flag_hash: 12,
read_only_checksum: Some(16),
payload_length: 20,
checksum: 24,
header_size: 28,
num_reservations: None,
num_stub_keys: None,
};
let mut data = Vec::new();
data.extend_from_slice(&0xC0DE_0000u32.to_le_bytes());
data.extend_from_slice(&0x11223344u32.to_le_bytes());
data.extend_from_slice(&999u32.to_le_bytes());
data.extend_from_slice(&0x55u32.to_le_bytes());
data.extend_from_slice(&0xABCDu32.to_le_bytes());
data.extend_from_slice(&7u32.to_le_bytes());
data.extend_from_slice(&0u32.to_le_bytes());
let h = Header::parse_with(&data, &layout).unwrap();
assert_eq!(h.payload_offset, 28);
assert_eq!(h.read_only_checksum, Some(0xABCD));
assert_eq!(h.payload_length, 7);
}
#[test]
fn module_flag_is_high_bit() {
let data = build_dummy(1, 0x8000_03e8, 0, 0, 0);
let h = Header::parse(&data).unwrap();
assert!(h.is_module());
assert_eq!(h.source_length(), 1000);
}
#[test]
fn rejects_non_cache() {
let data = b"not a jsc file at all........".to_vec();
assert!(Header::parse(&data).is_err());
}
}