///|
pub fn normalize_part_name(name : String) -> String {
let normalized = name.replace_all(old="\\", new="/")
let parts : Array[String] = []
for view in normalized.split("/") {
let part = view.to_owned()
if part == "" || part == "." {
continue
}
if part == ".." {
// Never allow a relative target to climb above the package root. Keeping
// the unresolved parent makes the result unusable as a package part,
// rather than silently retargeting it to an unrelated root part.
match parts.last() {
Some(previous) if previous != ".." => ignore(parts.pop())
_ => parts.push(part)
}
} else {
parts.push(part)
}
}
parts.join("/")
}
///|
pub fn relationships_part_name(source_part : String) -> String {
let source = normalize_part_name(source_part)
if source == "" {
"_rels/.rels"
} else {
match source.rev_find("/") {
Some(index) => {
let directory = source.unsafe_substring(start=0, end=index)
let filename = source.unsafe_substring(
start=index + 1,
end=source.length(),
)
directory + "/_rels/" + filename + ".rels"
}
None => "_rels/" + source + ".rels"
}
}
}
///|
pub fn resolve_target(
source_part : String,
target : String,
) -> String raise PackageError {
if target.has_prefix("/") {
let resolved = normalize_part_name(target)
if resolved == ".." || resolved.has_prefix("../") {
raise PackageError::InvalidPackage(
"relationship target traverses above package root: " + target,
)
}
return resolved
}
let source = normalize_part_name(source_part)
let directory = match source.rev_find("/") {
Some(index) => source.unsafe_substring(start=0, end=index + 1)
None => ""
}
let resolved = normalize_part_name(directory + target)
if resolved == ".." || resolved.has_prefix("../") {
raise PackageError::InvalidPackage(
"relationship target traverses above package root: " + target,
)
}
resolved
}
///|
fn parse_relationships(text : String) -> Array[Relationship] raise PackageError {
let root = @xml.parse(text) catch {
_ => raise PackageError::InvalidPackage("invalid relationships XML")
}
let relationships : Array[Relationship] = []
for element in root.children_named("Relationship") {
let id = element.attribute("Id").unwrap_or("")
let relationship_type = element.attribute("Type").unwrap_or("")
let target = element.attribute("Target").unwrap_or("")
if id == "" || relationship_type == "" || target == "" {
raise PackageError::InvalidPackage(
"relationship is missing Id, Type, or Target",
)
}
for existing in relationships {
if existing.id == id {
raise PackageError::InvalidPackage("duplicate relationship Id: " + id)
}
}
relationships.push({
id,
relationship_type,
target,
external: element.attribute("TargetMode") == Some("External"),
})
}
relationships
}
///|
fn unsafe_archive_name(name : String) -> Bool {
if name.has_prefix("/") || name.has_prefix("\\") {
return true
}
for part in name.replace_all(old="\\", new="/").split("/") {
if part == ".." {
return true
}
}
false
}
///|
fn open_package_with_policy(
content : FixedArray[Byte],
policy : ArchivePolicy,
) -> Package raise PackageError {
let infos = preflight_archive(content, policy)
let entries = extract_verified_archive(content, policy, infos)
let parts : Array[Part] = []
for entry in entries {
let (name, part_content) = entry
if unsafe_archive_name(name) {
raise PackageError::InvalidPackage("unsafe ZIP entry path: " + name)
}
let normalized = normalize_part_name(name)
if normalized == "" || name.has_suffix("/") {
continue
}
for existing in parts {
if existing.name == normalized {
raise PackageError::InvalidPackage("duplicate part: " + normalized)
}
}
parts.push({ name: normalized, content: part_content })
}
let archive : Package = { parts, }
if !archive.has_part("[Content_Types].xml") {
raise PackageError::InvalidPackage("missing [Content_Types].xml")
}
archive
}
///|
fn save_package(archive : Package) -> FixedArray[Byte] raise PackageError {
if !archive.has_part("[Content_Types].xml") {
raise PackageError::InvalidPackage("missing [Content_Types].xml")
}
let entries = []
for part in archive.parts {
if unsafe_archive_name(part.name) || normalize_part_name(part.name) == "" {
raise PackageError::InvalidPackage("invalid part path: " + part.name)
}
entries.push((normalize_part_name(part.name), part.content))
}
@fzip.zip_sync(entries)
}