///|
fn empty_element(uri : String, name : String) -> XmlElement {
{ uri, name, attributes: [], content: [], }
}
///|
pub fn propfind_names(
raw_path : String,
depth : Int,
properties : Array[(String, String)],
names_only? : Bool = false,
) -> Request raise DavError {
if depth != 0 && depth != 1 {
raise Invalid("PROPFIND depth must be 0 or 1")
}
if properties.length() > 256 || (names_only && !properties.is_empty()) {
raise Invalid("invalid property selection")
}
let body = StringBuilder()
let mut size = 0
body.write_string(
"",
)
if names_only {
body.write_string("")
} else if properties.is_empty() {
body.write_string("")
} else {
body.write_string("")
for (uri, name) in properties {
let xml = empty_element(uri, name).to_xml()
size += xml.length()
if size > 1048000 {
raise Invalid("property selection size limit")
}
body.write_string(xml)
}
body.write_string("")
}
body.write_string("")
{
verb: "PROPFIND",
target: path(raw_path),
headers: {
"Depth": depth.to_string(),
"Content-Type": "application/xml; charset=utf-8",
},
body: body.to_string(),
}
}
///|
pub fn set_properties(
raw_path : String,
set : Array[XmlElement],
remove : Array[(String, String)],
) -> Request raise DavError {
if set.length() + remove.length() < 1 || set.length() + remove.length() > 256 {
raise Invalid("property count 1..256")
}
let body = StringBuilder()
body.write_string(
"",
)
let mut size = 0
if !set.is_empty() {
body.write_string("")
for item in set {
let xml = item.to_xml()
size += xml.length()
if size > 1048000 {
raise Invalid("property update size limit")
}
body.write_string(xml)
}
body.write_string("")
}
if !remove.is_empty() {
body.write_string("")
for (uri, name) in remove {
let xml = empty_element(uri, name).to_xml()
size += xml.length()
if size > 1048000 {
raise Invalid("property update size limit")
}
body.write_string(xml)
}
body.write_string("")
}
body.write_string("")
{
verb: "PROPPATCH",
target: path(raw_path),
headers: { "Content-Type": "application/xml; charset=utf-8" },
body: body.to_string(),
}
}
///|
fn lock_timeout(value : String) -> Unit raise DavError {
if value == "Infinite" {
return
}
if !value.has_prefix("Second-") {
raise Invalid("lock timeout must be Infinite or Second-N")
}
let count = value[7:].to_owned()
if count.is_empty() ||
count.length() > 10 ||
!count.iter().all(c => c >= '0' && c <= '9') {
raise Invalid("invalid lock timeout")
}
ignore(
@strconv.parse_uint(count) catch {
_ => raise Invalid("lock timeout uint32 overflow")
},
)
}
///|
fn coded_url(value : String) -> Unit raise DavError {
if value.is_empty() ||
value.length() > 8192 ||
!value
.iter()
.all(c => {
c > ' ' && c < '\u007f' && c != '<' && c != '>' && c != '"' && c != '\\'
}) {
raise Invalid("invalid absolute coded URL")
}
let cs = value.to_array()
if !((cs[0] >= 'a' && cs[0] <= 'z') || (cs[0] >= 'A' && cs[0] <= 'Z')) {
raise Invalid("URI scheme required")
}
let mut colon = 0
while colon < cs.length() && cs[colon] != ':' {
let c = cs[colon]
if !((c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '+' ||
c == '-' ||
c == '.') {
raise Invalid("invalid URI scheme")
}
colon += 1
}
if colon == cs.length() || colon + 1 == cs.length() {
raise Invalid("absolute URI required")
}
}
///|
pub fn if_tokens(tokens : Array[String]) -> String raise DavError {
if tokens.is_empty() || tokens.length() > 64 {
raise Invalid("lock token count 1..64")
}
let list = []
for token in tokens {
coded_url(token)
list.push("<" + token + ">")
}
let value = "(" + list.join(" ") + ")"
if value.length() > 65536 {
raise Invalid("If header size limit")
}
value
}
///|
/// Tagged lists: tokens within one resource list are ANDed. Resource lists are separate conditions.
pub fn if_resources(
conditions : Array[(String, Array[String])],
) -> String raise DavError {
if conditions.is_empty() || conditions.length() > 64 {
raise Invalid("condition count 1..64")
}
let out = []
let used : Map[String, Bool] = Map([])
for (uri, tokens) in conditions {
coded_url(uri)
if used.contains(uri) {
raise Invalid("duplicate tagged resource")
}
used[uri] = true
out.push("<" + uri + "> " + if_tokens(tokens))
}
let value = out.join(" ")
if value.length() > 65536 {
raise Invalid("If header size limit")
}
value
}
///|
pub fn lock_request(
raw_path : String,
owner : String,
depth? : String = "infinity",
timeout? : String = "Second-3600",
shared? : Bool = false,
) -> Request raise DavError {
if depth != "0" && depth != "infinity" {
raise Invalid("LOCK depth must be 0 or infinity")
}
lock_timeout(timeout)
xml_text(owner)
if owner.length() > 8192 {
raise Invalid("lock owner size limit")
}
let scope = if shared { "shared" } else { "exclusive" }
{
verb: "LOCK",
target: path(raw_path),
headers: {
"Depth": depth,
"Timeout": timeout,
"Content-Type": "application/xml; charset=utf-8",
},
body: "" +
escape(owner) +
"",
}
}
///|
pub fn refresh_lock(
raw_path : String,
token : String,
timeout? : String = "Second-3600",
) -> Request raise DavError {
lock_timeout(timeout)
{
verb: "LOCK",
target: path(raw_path),
headers: { "If": if_tokens([token]), "Timeout": timeout },
body: "",
}
}
///|
pub fn unlock_request(
raw_path : String,
token : String,
) -> Request raise DavError {
coded_url(token)
{
verb: "UNLOCK",
target: path(raw_path),
headers: { "Lock-Token": "<" + token + ">" },
body: "",
}
}
///|
pub(all) struct LockInfo {
token : String?
root : String?
owner : XmlElement?
scope : String
depth : String
timeout : String?
} derive(Debug, Eq, ToJson)
///|
fn lock_info(node : XmlElement) -> LockInfo raise DavError {
let kinds = element_children(one(node, "locktype"))
if kinds.length() != 1 || kinds[0].uri != "DAV:" || kinds[0].name != "write" {
raise Invalid("unsupported DAV lock type")
}
let scopes = element_children(one(node, "lockscope"))
if scopes.length() != 1 ||
scopes[0].uri != "DAV:" ||
(scopes[0].name != "exclusive" && scopes[0].name != "shared") {
raise Invalid("invalid DAV lock scope")
}
let depth = one(node, "depth").text_content().trim().to_owned()
if depth != "0" && depth != "infinity" {
raise Invalid("invalid lock discovery depth")
}
let timeout = optional_text(node, "timeout")
if timeout is Some(value) {
lock_timeout(value)
}
let token = match children(node, "locktoken") {
[] => None
[t] => {
let value = one(t, "href").text_content().trim().to_owned()
coded_url(value)
Some(value)
}
_ => raise Invalid("duplicate lock token")
}
let root = match children(node, "lockroot") {
[] => None
[r] => Some(one(r, "href").text_content().trim().to_owned())
_ => raise Invalid("duplicate lock root")
}
let owner = match children(node, "owner") {
[] => None
[o] => Some(o)
_ => raise Invalid("duplicate lock owner")
}
{ token, root, owner, scope: scopes[0].name, depth, timeout, }
}
///|
pub fn parse_locks(source : String) -> Array[LockInfo] raise DavError {
let root = parse_xml(source)
let discovery = if root.uri == "DAV:" && root.name == "prop" {
one(root, "lockdiscovery")
} else if root.uri == "DAV:" && root.name == "lockdiscovery" {
root
} else {
raise Invalid("lockdiscovery response required")
}
children(discovery, "activelock").map(lock_info)
}