///|
// ZIP32 only. All arithmetic operands are bounded before they are added.
fn ods_zip_u16(data : Bytes, pos : Int32) -> Int32 raise OdsError {
if pos < 0 || pos > data.length() - 2 {
raise Invalid("truncated ZIP")
}
data[pos].to_int() | (data[pos + 1].to_int() << 8)
}
///|
fn ods_zip_u32(data : Bytes, pos : Int32) -> Int32 raise OdsError {
ods_zip_u16(data, pos) | (ods_zip_u16(data, pos + 2) << 16)
}
///|
fn ods_zip_size(data : Bytes, pos : Int32) -> Int32 raise OdsError {
let n = ods_zip_u32(data, pos)
if n < 0 || n > 33554432 {
raise Invalid("unsupported ZIP size or ZIP64")
}
n
}
///|
fn ods_zip_extra(
data : Bytes,
start : Int32,
length : Int32,
) -> Unit raise OdsError {
let end = start + length
if end > data.length() {
raise Invalid("truncated ZIP extra field")
}
let mut p = start
while p < end {
if end - p < 4 {
raise Invalid("invalid ZIP extra field")
}
let tag = ods_zip_u16(data, p)
let size = ods_zip_u16(data, p + 2)
if tag == 1 {
raise Invalid("ZIP64 is unsupported")
}
// Unicode Path metadata may supply a second interpretation of an otherwise
// allowlisted raw filename. Never accept competing path identities.
if tag == 0x7075 {
raise Invalid("ZIP Unicode path aliases are unsupported")
}
p = p + 4 + size
if p > end {
raise Invalid("truncated ZIP extra field")
}
}
}
///|
fn ods_zip_utf8(data : Bytes) -> String raise OdsError {
@utf8.decode(data[:]) catch {
_ => raise Invalid("invalid ZIP/XML UTF-8")
}
}
///|
fn ods_zip_inflate(data : Bytes, budget : Int32) -> Bytes raise OdsError {
let decoder = @flate.Inflater()
let scratch = FixedArray::make(8192, b'\x00')
let out = @buffer.Buffer()
let mut consumed = 0
for ;; {
let status = decoder.step(data[consumed:], scratch.mut_view(), end=true) catch {
_ => raise Invalid("invalid DEFLATE stream")
}
let used = decoder.last_consumed()
let produced = decoder.last_produced()
if produced > budget - out.length() {
raise Invalid("ODS exceeds 4 MiB inflated budget")
}
for i in 0.. Bool raise OdsError {
let count = ods_zip_u16(data, end + 10)
if count == 0 ||
count > 128 ||
ods_zip_u16(data, end + 4) != 0 ||
ods_zip_u16(data, end + 6) != 0 ||
ods_zip_u16(data, end + 8) != count {
return false
}
let size = ods_zip_u32(data, end + 12)
let start = ods_zip_u32(data, end + 16)
if size < 0 || size > end || start < 0 || start != end - size {
return false
}
let mut p = start
for _ in 0.. end - 46 || ods_zip_u32(data, p) != 0x02014b50 {
return false
}
let name_len = ods_zip_u16(data, p + 28)
let next = p +
46 +
name_len +
ods_zip_u16(data, p + 30) +
ods_zip_u16(data, p + 32)
if name_len == 0 || next > end {
return false
}
p = next
}
p == end
}
///|
// Every archive entry is checked, even entries not used by the XML reader.
// No filesystem access, extraction paths, or unbounded inflate convenience API.
fn read_ods_content_noticing(
data : Bytes,
omitted : Array[String],
style_parts~ : Array[String]? = None,
require_manifest~ : Bool = false,
id_assignments~ : Array[XmlIdAssignment] = [],
id_diagnostics~ : Array[XmlIdDiagnostic] = [],
processor_events~ : Array[XmlProcessorEvent] = [],
) -> String raise OdsError {
let length = data.length()
if length < 22 || length > 33554432 {
raise Invalid("invalid ODS ZIP input size")
}
let mut cursor = length - 22
let min_end = if length > 65557 { length - 65557 } else { 0 }
let mut fallback = -1
let mut selected = -1
while cursor >= min_end {
if ods_zip_u32(data, cursor) == 0x06054b50 &&
cursor + 22 + ods_zip_u16(data, cursor + 20) == length {
if fallback == -1 {
fallback = cursor
}
if ods_zip_end_candidate(data, cursor) {
if selected != -1 {
raise Invalid("ambiguous ZIP end records")
}
selected = cursor
}
}
cursor = cursor - 1
}
// Keep the original diagnostics for archives with no valid framing candidate.
let end = if selected >= 0 { selected } else { fallback }
if end < 0 {
raise Invalid("ZIP end record missing")
}
let count = ods_zip_u16(data, end + 10)
if ods_zip_u16(data, end + 4) != 0 ||
ods_zip_u16(data, end + 6) != 0 ||
ods_zip_u16(data, end + 8) != count {
raise Invalid("multi-disk ZIP is unsupported")
}
if count == 0 || count > 128 {
raise Invalid("ZIP entry count exceeds budget")
}
let cd_size = ods_zip_size(data, end + 12)
let cd_start = ods_zip_size(data, end + 16)
if cd_start + cd_size != end {
raise Invalid("invalid ZIP directory or ZIP64")
}
let names : Array[String] = []
let spans : Array[(Int32, Int32)] = []
let mut covered = 0
let mut p = cd_start
let mut total = 0
let mut content : Bytes? = None
let mut manifest : String? = None
let extra_xml : Map[String, Bytes] = Map([])
let mut mimetype = false
for _ in 0.. end - 46 || ods_zip_u32(data, p) != 0x02014b50 {
raise Invalid("invalid ZIP directory")
}
let version = ods_zip_u16(data, p + 6)
let flags = ods_zip_u16(data, p + 8)
let compression = ods_zip_u16(data, p + 10)
if version > 20 ||
(flags & 0xF7F1) != 0 ||
(compression != 0 && compression != 8) ||
(compression == 0 && (flags & 6) != 0) {
raise Invalid("encrypted or unsupported ZIP entry")
}
let crc = ods_zip_u32(data, p + 16)
let packed = ods_zip_size(data, p + 20)
let unpacked = ods_zip_size(data, p + 24)
if unpacked > 4194304 - total {
raise Invalid("ODS exceeds 4 MiB inflated budget")
}
let name_len = ods_zip_u16(data, p + 28)
let extra_len = ods_zip_u16(data, p + 30)
let comment_len = ods_zip_u16(data, p + 32)
if ods_zip_u16(data, p + 34) != 0 {
raise Invalid("multi-disk ZIP entry")
}
let next = p + 46 + name_len + extra_len + comment_len
if name_len == 0 || next > end {
raise Invalid("invalid ZIP entry name")
}
let name_bytes = data[p + 46:p + 46 + name_len].to_owned()
let name = ods_zip_utf8(name_bytes)
ods_package_path(name)
if name.has_suffix("/") && unpacked != 0 {
raise Invalid("nonempty ZIP directory entry")
}
if names.contains(name) {
raise Invalid("duplicate ZIP entry")
}
names.push(name)
ods_zip_extra(data, p + 46 + name_len, extra_len)
let local_start = ods_zip_size(data, p + 42)
if local_start > cd_start - 30 ||
ods_zip_u32(data, local_start) != 0x04034b50 {
raise Invalid("invalid ZIP local header")
}
if ods_zip_u16(data, local_start + 4) != version ||
ods_zip_u16(data, local_start + 6) != flags ||
ods_zip_u16(data, local_start + 8) != compression ||
ods_zip_u16(data, local_start + 26) != name_len {
raise Invalid("ZIP local/directory mismatch")
}
let local_extra = ods_zip_u16(data, local_start + 28)
let body = local_start + 30 + name_len + local_extra
if body + packed > cd_start {
raise Invalid("truncated ZIP entry")
}
if data[local_start + 30:local_start + 30 + name_len].to_owned() !=
name_bytes {
raise Invalid("ZIP local name mismatch")
}
ods_zip_extra(data, local_start + 30 + name_len, local_extra)
let local_crc = ods_zip_u32(data, local_start + 14)
let local_packed = ods_zip_size(data, local_start + 18)
let local_unpacked = ods_zip_size(data, local_start + 22)
let mut finish = body + packed
if (flags & 8) == 0 {
if local_crc != crc ||
local_packed != packed ||
local_unpacked != unpacked {
raise Invalid("ZIP local size/CRC mismatch")
}
} else {
if (local_crc != 0 && local_crc != crc) ||
(local_packed != 0 && local_packed != packed) ||
(local_unpacked != 0 && local_unpacked != unpacked) {
raise Invalid("ZIP descriptor header mismatch")
}
// Match the complete tuple against the directory, not just the first
// word: an unsigned descriptor's CRC may equal the optional signature.
let unsigned_matches = finish + 12 <= cd_start &&
ods_zip_u32(data, finish) == crc &&
ods_zip_u32(data, finish + 4) == packed &&
ods_zip_u32(data, finish + 8) == unpacked
let signed_matches = finish + 16 <= cd_start &&
ods_zip_u32(data, finish) == 0x08074b50 &&
ods_zip_u32(data, finish + 4) == crc &&
ods_zip_u32(data, finish + 8) == packed &&
ods_zip_u32(data, finish + 12) == unpacked
finish = if signed_matches {
finish + 16
} else if unsigned_matches {
finish + 12
} else {
raise Invalid("invalid ZIP data descriptor")
}
}
for (a, b) in spans {
if local_start < b && finish > a {
raise Invalid("overlapping ZIP entries")
}
}
spans.push((local_start, finish))
covered = covered + finish - local_start
let raw = data[body:body + packed].to_owned()
let decoded = if compression == 0 {
if packed != unpacked {
raise Invalid("invalid stored ZIP size")
}
raw
} else {
ods_zip_inflate(raw, 4194304 - total)
}
if decoded.length() != unpacked {
raise Invalid("forged ZIP uncompressed size")
}
if @checksum.crc32(decoded[:]).reinterpret_as_int() != crc {
raise Invalid("ZIP CRC mismatch")
}
total = total + decoded.length()
if name == "mimetype" {
if compression != 0 ||
local_start != 0 ||
local_extra != 0 ||
decoded != @utf8.encode(media_type[:]) {
raise Invalid("invalid ODS mimetype")
}
mimetype = true
} else if name == "content.xml" {
content = Some(decoded)
} else if name == "styles.xml" {
extra_xml.set(name, decoded)
if style_parts is None {
omitted.push(name)
}
} else if name == "meta.xml" || name == "settings.xml" {
extra_xml.set(name, decoded)
omitted.push(name)
} else if name == "META-INF/manifest.xml" {
manifest = Some(ods_xml_decode(decoded))
} else if name != "Configurations2/" {
if name.has_prefix("META-INF/") && name.contains("signatures") {
extra_xml.set(name, decoded)
}
omitted.push(name)
}
p = next
}
if p != end || covered != cd_start || !mimetype {
raise Invalid("invalid ODS ZIP structure")
}
if require_manifest {
match manifest {
Some(xml) =>
check_package_manifest(
xml,
names,
id_assignments~,
id_diagnostics~,
processor_events~,
)
None => raise Invalid("ODS package manifest missing")
}
}
// Manifest encryption is diagnosed before attempting to decode ciphertext.
for name, raw_xml in extra_xml {
let xml = ods_xml_decode(raw_xml)
if name == "styles.xml" {
if style_parts is Some(parts) {
parts.push(xml)
}
}
if require_manifest {
ignore(
read_xml(
xml,
document_part=name,
id_assignments~,
id_diagnostics~,
processor_events~,
),
)
}
}
match content {
Some(raw_xml) => ods_xml_decode(raw_xml)
None => raise Invalid("ODS content.xml missing")
}
}
///|
// Paths are never extracted or resolved. Reject ambiguous traversal identities
// before consuming payloads; Unicode safe relative names remain literal names.
fn ods_package_path(name : String) -> Unit raise OdsError {
if name == "" ||
name.has_prefix("/") ||
name.contains("\\") ||
name.contains(":") {
raise Invalid("unsafe ODS package path")
}
for c in name {
if c.to_int() < 32 || c.to_int() == 127 {
raise Invalid("control in ODS package path")
}
}
let segments = name.split("/").map(s => s.to_owned()).collect()
for i, segment in segments {
if segment == "." ||
segment == ".." ||
(segment == "" && i != segments.length() - 1) {
raise Invalid("unsafe ODS package path component")
}
}
}