///|
// Conservative ASCII URI subset; encoded and alias cases are refused.
fn valid_segment(segment : String) -> Bool {
if segment == "" ||
segment == "." ||
segment == ".." ||
segment.has_suffix(".") {
return false
}
for ch in segment {
if !(ch >= 'a' && ch <= 'z') &&
!(ch >= 'A' && ch <= 'Z') &&
!(ch >= '0' && ch <= '9') &&
ch != '_' &&
ch != '-' &&
ch != '.' &&
ch != '[' &&
ch != ']' {
return false
}
}
true
}
///|
fn check_name(name : String) -> Unit raise {
guard name.length() <= 1024 && !name.has_prefix("/") else {
raise Refused("invalid or overlong part name")
}
guard name == "[Content_Types].xml" || !name.contains_any(chars="[]") else {
raise Refused("reserved URI characters in part name")
}
for segment in name.split("/") {
guard valid_segment(segment.to_owned()) else {
raise Refused("ambiguous or unsupported part URI")
}
}
}
///|
pub fn resolve_target(source : String, target : String) -> String raise {
guard target.length() > 0 && target.length() <= 1024 else {
raise Refused("empty or overlong relationship target")
}
let segments : Array[String] = []
if !target.has_prefix("/") && source != "/" {
check_name(source)
for segment in source.split("/") {
segments.push(segment.to_owned())
}
ignore(segments.pop())
}
let path = if target.has_prefix("/") { target[1:].to_owned() } else { target }
for segment in path.split("/") {
let s = segment.to_owned()
if s == ".." {
guard segments.length() > 0 else {
raise Refused("target escapes package root")
}
ignore(segments.pop())
} else if s == "." {
continue
} else {
guard valid_segment(s) else {
raise Refused("unsupported target URI encoding or syntax")
}
segments.push(s)
}
}
let result = segments.join("/")
check_name(result)
result
}
///|
fn relationship_source(name : String) -> String raise {
if name == "_rels/.rels" {
return "/"
}
guard name.has_suffix(".rels") else {
raise Refused("invalid relationships part")
}
let segments = name.split("/").map(fn(s) { s.to_owned() }).collect()
let n = segments.length()
guard n >= 2 && segments[n - 2] == "_rels" else {
raise Refused("invalid relationships part location")
}
let source : Array[String] = []
for i in 0..<(n - 2) {
source.push(segments[i])
}
source.push(segments[n - 1][:segments[n - 1].length() - 5].to_owned())
source.join("/")
}
///|
fn relationship_part(source : String) -> String {
if source == "/" {
return "_rels/.rels"
}
match source.rev_split_once("/") {
Some((dir, file)) => dir.to_owned() + "/_rels/" + file.to_owned() + ".rels"
None => "_rels/" + source + ".rels"
}
}