///|
/// Parse the variable-length integers used by CHM PMGL chunks.
fn chm_quick_u32(reader : Reader) -> (Reader, Int) raise MspackError {
let mut r = reader
let mut value = 0
let mut shift = 0
let mut done = false
while !done {
if shift >= 32 {
raise MspackError("CHM variable integer is too large")
}
let (next, byte) = r.u8()
r = next
value = value | ((byte & 0x7F) << shift)
if (byte & 0x80) == 0 {
done = true
} else {
shift = shift + 7
}
}
(r, value)
}
///|
fn chm_chunk_magic(data : Bytes, offset : Int) -> String raise MspackError {
checked_range(data.length(), offset, 4)
let r = Reader::new(data).seek(offset)
let (r, a) = r.u8()
let (r, b) = r.u8()
let (r, c) = r.u8()
let (_, d) = r.u8()
if a == 0x50 && b == 0x4D && c == 0x47 && d == 0x4C {
"PMGL"
} else if a == 0x50 && b == 0x4D && c == 0x47 && d == 0x49 {
"PMGI"
} else {
"????"
}
}
///|
/// Parse one uncompressed PMGL listing chunk.
fn parse_pmgl_chunk(
data : Bytes,
offset : Int,
chunk_size : Int,
limits : Limits,
) -> Array[ArchiveEntry] raise MspackError {
checked_range(data.length(), offset, chunk_size)
if chm_chunk_magic(data, offset) != "PMGL" {
raise MspackError("invalid CHM listing chunk")
}
let r = Reader::new(data).seek(offset + 4)
let (r, free_space) = r.u32le()
let used = chunk_size - 8 - free_space.to_int()
if free_space.to_int() > chunk_size - 8 || used < 0 {
raise MspackError("invalid CHM listing free space")
}
let end = offset + 8 + used
let mut cursor = r
let entries : Array[ArchiveEntry] = []
while cursor.at() < end {
let (next, name_len) = chm_quick_u32(cursor)
cursor = next
if name_len <= 0 ||
name_len > limits.max_entries ||
cursor.at() + name_len > end {
raise MspackError("invalid CHM listing name")
}
let (next, name_bytes) = cursor.bytes(name_len)
cursor = next
let name = name_bytes.to_string()
let (next, section) = chm_quick_u32(cursor)
cursor = next
let (next, start) = chm_quick_u32(cursor)
cursor = next
let (next, length) = chm_quick_u32(cursor)
cursor = next
if section > 0xFFFF || start < 0 || length < 0 {
raise MspackError("unsupported CHM listing entry")
}
entries.push({
name,
size: length,
compressed_size: length,
offset: start,
folder: section,
flags: 0,
})
if entries.length() > limits.max_entries {
raise MspackError("archive limit exceeded")
}
}
entries
}
///|
/// Enumerate uncompressed CHM PMGL entries in the directory stream.
pub fn list_chm_directory(
data : Bytes,
header : ChmHeader,
directory : ChmDirectory,
limits? : Limits = default_limits,
) -> Array[ArchiveEntry] raise MspackError {
if directory.chunk_size <= 0 || directory.listing_length <= 0 {
[]
} else {
let chunks = directory.listing_length / directory.chunk_size
if chunks > limits.max_entries {
raise MspackError("CHM chunk count exceeds limit")
}
let entries : Array[ArchiveEntry] = []
for index in 0.. header.dir_offset + header.dir_length {
raise MspackError("CHM listing exceeds directory")
}
if chm_chunk_magic(data, offset) == "PMGL" {
let chunk_entries = parse_pmgl_chunk(
data,
offset,
directory.chunk_size,
limits,
)
for entry in chunk_entries {
entries.push(entry)
if entries.length() > limits.max_entries {
raise MspackError("archive limit exceeded")
}
}
}
}
entries
}
}
///|
/// Read a CHM header and enumerate its uncompressed directory entries.
pub fn read_chm_entries(
data : Bytes,
limits? : Limits = default_limits,
) -> ArchiveListing raise MspackError {
let (header, directory) = inspect_chm(data)
let entries = list_chm_directory(data, header, directory, limits~)
{
format: "CHM",
entries,
warnings: ["compressed CHM content requires LZX decoding"],
}
}
///|
/// Extract an entry from an uncompressed CHM section.
pub fn extract_chm_entry(
data : Bytes,
header : ChmHeader,
entry : ArchiveEntry,
limits? : Limits = default_limits,
) -> Bytes raise MspackError {
if entry.folder != 0 {
raise MspackError("compressed CHM sections require LZX decoding")
}
if entry.size < 0 || entry.size > limits.max_output {
raise MspackError("CHM entry exceeds output limit")
}
let start = header.data_offset + entry.offset
checked_range(data.length(), start, entry.size)
let output : Array[Byte] = []
for i in 0.. ChmSection raise MspackError {
if index < 0 || length < 0 || length > limits.max_output {
raise MspackError("invalid CHM section")
}
let offset = header.data_offset + index * 0x1000
if offset < header.data_offset {
raise MspackError("CHM section offset overflow")
}
{ index, offset, length, }
}
///|
/// Read a bounded range from a CHM section.
pub fn read_chm_section(
data : Bytes,
section : ChmSection,
relative : Int,
length : Int,
limits? : Limits = default_limits,
) -> Bytes raise MspackError {
if relative < 0 || length < 0 || length > limits.max_output {
raise MspackError("invalid CHM section range")
}
checked_range(section.length, relative, length)
checked_range(data.length(), section.offset + relative, length)
let out : Array[Byte] = []
for i in 0..