Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+634
View File
@@ -0,0 +1,634 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use types::collection::Collection;
use crate::utils::server::TestServer;
use crate::utils::webdav::GenerateTestDavResource;
use crate::webdav::*;
pub async fn test(test: &TestServer) {
println!("Running PUT/GET tests...");
let client = test.account("[email protected]").webdav_client();
// Simple PUT
let mut files = AHashMap::new();
for (path, ct, content) in [
(
"/dav/file/john%40example.com/file1.txt",
"text/plain",
TEST_FILE_1,
),
(
"/dav/file/john%40example.com/file2.txt",
"text/x-other",
TEST_FILE_2,
),
(
"/dav/card/john%40example.com/default/card1.vcf",
"text/vcard; charset=utf-8",
TEST_VCARD_1,
),
(
"/dav/card/john%40example.com/default/card2.vcf",
"text/vcard; charset=utf-8",
TEST_VCARD_2,
),
(
"/dav/cal/john%40example.com/default/event1.ics",
"text/calendar; charset=utf-8",
TEST_ICAL_1,
),
(
"/dav/cal/john%40example.com/default/event2.ics",
"text/calendar; charset=utf-8",
TEST_ICAL_2,
),
] {
let content = content.replace("\n", "\r\n");
let etag = client
.request_with_headers("PUT", path, [("content-type", ct)], &content)
.await
.with_status(StatusCode::CREATED)
.etag()
.to_string();
files.insert(path, (content, ct, etag));
}
// Test GET
for (path, (content, ct, etag)) in &files {
client
.request("GET", path, "")
.await
.with_status(StatusCode::OK)
.with_header("etag", etag)
.with_header("content-type", ct)
.with_body(content);
}
// Test GET with a Range header
let path = "/dav/file/john%40example.com/file1.txt";
let (content, _, etag) = files.get(path).unwrap();
let size = content.len();
for (range, expect_content_range, expect_body) in [
("bytes=0-4", format!("bytes 0-4/{size}"), &content[..5]),
("bytes=0-0", format!("bytes 0-0/{size}"), &content[..1]),
(
"bytes=5-",
format!("bytes 5-{}/{size}", size - 1),
&content[5..],
),
(
"bytes=-6",
format!("bytes {}-{}/{size}", size - 6, size - 1),
&content[size - 6..],
),
(
"bytes=0-100000",
format!("bytes 0-{}/{size}", size - 1),
&content[..],
),
] {
client
.request_with_headers("GET", path, [("range", range)], "")
.await
.with_status(StatusCode::PARTIAL_CONTENT)
.with_header("content-range", &expect_content_range)
.with_header("content-length", &expect_body.len().to_string())
.with_header("accept-ranges", "bytes")
.with_header("etag", etag)
.with_body(expect_body);
}
// Ranges outside the resource should fail
for range in ["bytes=100000-", &format!("bytes={size}-"), "bytes=-0"] {
client
.request_with_headers("GET", path, [("range", range)], "")
.await
.with_status(StatusCode::RANGE_NOT_SATISFIABLE)
.with_header("content-range", &format!("bytes */{size}"));
}
// Multiple, invalid or unknown ranges should be ignored
for range in ["bytes=0-4,6-8", "items=0-4", "bytes=4-2", "bytes=abc"] {
client
.request_with_headers("GET", path, [("range", range)], "")
.await
.with_status(StatusCode::OK)
.with_header("accept-ranges", "bytes")
.with_body(content);
}
// Ranges should be ignored on HEAD requests
client
.request_with_headers("HEAD", path, [("range", "bytes=0-4")], "")
.await
.with_status(StatusCode::OK)
.with_header("content-length", &size.to_string())
.with_empty_body();
// Ranges should only be served when the If-Range validator matches
let weak_etag = format!("W/{etag}");
let last_modified = client
.request("HEAD", path, "")
.await
.with_status(StatusCode::OK)
.header("last-modified")
.to_string();
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
for (if_range, expect_status) in [
(etag.as_str(), StatusCode::PARTIAL_CONTENT),
(last_modified.as_str(), StatusCode::PARTIAL_CONTENT),
(weak_etag.as_str(), StatusCode::OK),
("\"invalid-etag\"", StatusCode::OK),
("Sun, 09 Aug 2020 12:00:00 GMT", StatusCode::OK),
] {
client
.request_with_headers(
"GET",
path,
[("range", "bytes=0-4"), ("if-range", if_range)],
"",
)
.await
.with_status(expect_status);
}
// If-Range without a Range header should be ignored
client
.request_with_headers("GET", path, [("if-range", "\"invalid-etag\"")], "")
.await
.with_status(StatusCode::OK)
.with_body(content);
// Ranges on empty files should be ignored
let empty_path = "/dav/file/john%40example.com/empty.txt";
client
.request_with_headers("PUT", empty_path, [("content-type", "text/plain")], "")
.await
.with_status(StatusCode::CREATED);
for range in ["bytes=0-4", "bytes=-5", "bytes=0-"] {
client
.request_with_headers("GET", empty_path, [("range", range)], "")
.await
.with_status(StatusCode::OK)
.with_header("accept-ranges", "bytes")
.with_empty_body();
}
client
.request("DELETE", empty_path, "")
.await
.with_status(StatusCode::NO_CONTENT);
// PUT under a non-existing parent should fail
for (path, contents) in [
("/dav/file/john%40example.com/foo/file1.txt", TEST_FILE_1),
("/dav/card/john%40example.com/foo/card1.vcf", TEST_VCARD_1),
("/dav/cal/john%40example.com/foo/event1.ics", TEST_ICAL_1),
] {
client
.request("PUT", path, contents)
.await
.with_status(StatusCode::CONFLICT);
}
// PUT under resources should fail
for (path, contents) in [
(
"/dav/file/john%40example.com/file1.txt/other-file.txt",
TEST_FILE_1,
),
(
"/dav/card/john%40example.com/default/card1.vcf/other-file.vcf",
TEST_VCARD_1,
),
(
"/dav/cal/john%40example.com/default/event1.ics/other-file.ical",
TEST_ICAL_1,
),
] {
client
.request("PUT", path, contents)
.await
.with_status(StatusCode::METHOD_NOT_ALLOWED);
}
// PUT a non-vCard/iCalendar file should fail
for (path, ct, content, precondition) in [
(
"/dav/card/john%40example.com/card3.vcf",
"text/vcard; charset=utf-8",
TEST_FILE_1,
"B:supported-address-data",
),
(
"/dav/cal/john%40example.com/event3.ics",
"text/calendar; charset=utf-8",
TEST_FILE_2,
"A:supported-calendar-data",
),
] {
client
.request_with_headers("PUT", path, [("content-type", ct)], content)
.await
.with_status(StatusCode::PRECONDITION_FAILED)
.with_failed_precondition(precondition, "");
}
// Exceeding the configured file limits should fail
let conf = &test.server.core.groupware;
for (path, contents, max_size, expect) in [
(
"/dav/file/john%40example.com/chunky-file1.txt",
TEST_FILE_1,
conf.max_file_size,
None,
),
(
"/dav/card/john%40example.com/chunky-card1.vcf",
TEST_VCARD_1,
conf.max_vcard_size,
Some("B:max-resource-size"),
),
(
"/dav/cal/john%40example.com/chunky-event1.ics",
TEST_ICAL_1,
conf.max_ical_size,
Some("A:max-resource-size"),
),
] {
let mut chunky_contents = String::with_capacity(max_size + contents.len());
while chunky_contents.len() < max_size {
chunky_contents.push_str(contents);
}
let response = client
.request("PUT", path, chunky_contents)
.await
.with_status(
expect
.map(|_| StatusCode::PRECONDITION_FAILED)
.unwrap_or(StatusCode::PAYLOAD_TOO_LARGE),
);
if let Some(expect) = expect {
response.with_failed_precondition(expect, &max_size.to_string());
}
}
// PUT requests cannot exceed quota
let mike_noquota = test.account("[email protected]").webdav_client();
for resource_type in [
DavResourceName::File,
DavResourceName::Card,
DavResourceName::Cal,
] {
let path = format!(
"{}/mike%40example.com/quota-test/",
resource_type.base_path()
);
mike_noquota
.mkcol("MKCOL", &path, [], [])
.await
.with_status(StatusCode::CREATED);
let mut num_success = 0;
let mut did_fail = false;
for i in 0..100 {
let content = resource_type.generate();
let available = mike_noquota.available_quota(&path).await;
let response = mike_noquota
.request_with_headers("PUT", &format!("{path}file{i}"), [], &content)
.await;
if available > content.len() as u64 {
num_success += 1;
response.with_status(StatusCode::CREATED);
} else {
response
.with_status(StatusCode::PRECONDITION_FAILED)
.with_failed_precondition("D:quota-not-exceeded", "");
did_fail = true;
break;
}
}
if !did_fail {
panic!("Quota test failed: {} files created", num_success);
}
if num_success == 0 {
panic!("Quota test failed: no files created");
}
mike_noquota
.request("DELETE", &path, "")
.await
.with_status(StatusCode::NO_CONTENT);
}
// PUT precondition enforcement
let modseq = [
test.resources("[email protected]", Collection::FileNode)
.await
.highest_change_id,
test.resources("[email protected]", Collection::Calendar)
.await
.highest_change_id,
test.resources("[email protected]", Collection::AddressBook)
.await
.highest_change_id,
];
for (path, ct, content) in [
(
"/dav/file/john%40example.com/file1.txt",
"text/plain",
TEST_FILE_1,
),
(
"/dav/card/john%40example.com/default/card1.vcf",
"text/vcard; charset=utf-8",
TEST_VCARD_1,
),
(
"/dav/cal/john%40example.com/default/event1.ics",
"text/calendar; charset=utf-8",
TEST_ICAL_1,
),
] {
let content = content.replace("\n", "\r\n");
client
.request_with_headers(
"PUT",
path,
[("content-type", ct), ("if-none-match", "*")],
&content,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED);
client
.request_with_headers(
"PUT",
path,
[("content-type", ct), ("overwrite", "F")],
&content,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED);
client
.request_with_headers(
"PUT",
path,
[("content-type", ct), ("if", "([\"3827\"])")],
&content,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED);
client
.request_with_headers(
"PUT",
path,
[
("content-type", ct),
("if", "([\"3827\"])"),
("prefer", "return=representation"),
],
&content,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED)
.with_header("preference-applied", "return=representation")
.with_body(&content);
}
assert_eq!(
[
test.resources("[email protected]", Collection::FileNode)
.await
.highest_change_id,
test.resources("[email protected]", Collection::Calendar)
.await
.highest_change_id,
test.resources("[email protected]", Collection::AddressBook)
.await
.highest_change_id,
],
modseq
);
// Update files using etags
for (path, (content, ct, etag)) in &mut files {
let condition = format!("([{}])", etag);
*content = content.replace("X-TEST:SEQ1", "X-TEST:SEQ2");
*etag = client
.request_with_headers(
"PUT",
path,
[("content-type", &**ct), ("if", condition.as_str())],
content.as_str(),
)
.await
.with_status(StatusCode::NO_CONTENT)
.etag()
.to_string();
}
// Test GET
for (path, (content, ct, etag)) in &files {
client
.request("GET", path, "")
.await
.with_status(StatusCode::OK)
.with_header("etag", etag)
.with_header("content-type", ct)
.with_body(content);
}
// PUT requests require unique UIDs
for (path, ct, content, precond_key, precond_value) in [
(
"/dav/card/john%40example.com/default/card5.vcf",
"text/vcard; charset=utf-8",
TEST_VCARD_1,
"B:no-uid-conflict.D:href",
"/dav/card/john%40example.com/default/card1.vcf",
),
(
"/dav/cal/john%40example.com/default/event5.ics",
"text/calendar; charset=utf-8",
TEST_ICAL_1,
"A:no-uid-conflict.D:href",
"/dav/cal/john%40example.com/default/event1.ics",
),
] {
client
.request_with_headers(
"PUT",
path,
[("content-type", ct), ("if-none-match", "*")],
content,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED)
.with_failed_precondition(precond_key, precond_value);
}
// iCal containing different component types should fail
client
.request_with_headers(
"PUT",
"/dav/cal/john%40example.com/default/invalid.ics",
[
("content-type", "text/calendar; charset=utf-8"),
("if-none-match", "*"),
],
r#"BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:1234567890
SUMMARY:Test Event
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
BEGIN:VTODO
UID:1234567890
SUMMARY:Test Task
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VTODO
END:VCALENDAR
"#,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED)
.with_failed_precondition("A:valid-calendar-object-resource", "");
// iCal referencing more than one UID should fail
client
.request_with_headers(
"PUT",
"/dav/cal/john%40example.com/default/invalid.ics",
[
("content-type", "text/calendar; charset=utf-8"),
("if-none-match", "*"),
],
r#"BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
UID:1234567890
SUMMARY:Test Event 1
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
BEGIN:VEVENT
UID:1234567891
SUMMARY:Test Event 2
DTSTART;TZID=Europe/London:20231001T120000
DTEND;TZID=Europe/London:20231001T130000
END:VEVENT
END:VCALENDAR
"#,
)
.await
.with_status(StatusCode::PRECONDITION_FAILED)
.with_failed_precondition("A:valid-calendar-object-resource", "");
// Deleting unknown/invalid destinations should fail
for (path, expect) in [
(
"/dav/file/john%40example.com/unknown.txt",
StatusCode::NOT_FOUND,
),
(
"/dav/card/john%40example.com/default/unknown.txt",
StatusCode::NOT_FOUND,
),
(
"/dav/cal/john%40example.com/default/unknown.txt",
StatusCode::NOT_FOUND,
),
("/dav/file/john%40example.com", StatusCode::FORBIDDEN),
("/dav/cal/john%40example.com", StatusCode::FORBIDDEN),
("/dav/card/john%40example.com", StatusCode::FORBIDDEN),
(
"/dav/pal/john%40example.com",
StatusCode::METHOD_NOT_ALLOWED,
),
("/dav/file", StatusCode::FORBIDDEN),
("/dav/cal", StatusCode::FORBIDDEN),
("/dav/card", StatusCode::FORBIDDEN),
("/dav/pal", StatusCode::METHOD_NOT_ALLOWED),
] {
client.request("DELETE", path, "").await.with_status(expect);
}
// Resource names containing characters that are legal in a path segment
for (resource_type, container, name) in [
(DavResourceName::Cal, "default", "foo+bar(1)&x:[email protected]"),
(DavResourceName::Card, "default", "foo+bar(1)&x:[email protected]"),
(DavResourceName::Cal, "cal+1(a)", "event.ics"),
(DavResourceName::Card, "book+1(a)", "card.vcf"),
] {
let container_path = format!(
"{}/john%40example.com/{container}",
resource_type.base_path()
);
if container != "default" {
client
.request("MKCOL", &container_path, "")
.await
.with_status(StatusCode::CREATED);
}
let path = format!("{container_path}/{name}");
let content = resource_type.generate();
client
.request("PUT", &path, &content)
.await
.with_status(StatusCode::CREATED);
client
.propfind(&path, ["D:getetag"])
.await
.with_hrefs([path.as_str()]);
client
.request("GET", &path, "")
.await
.with_status(StatusCode::OK)
.with_body(&content);
client
.request("DELETE", &path, "")
.await
.with_status(StatusCode::NO_CONTENT);
if container != "default" {
client
.request("DELETE", &container_path, "")
.await
.with_status(StatusCode::NO_CONTENT);
}
}
// Delete files
for (path, (_, _, etag)) in &files {
client
.request_with_headers("DELETE", path, [("if", "([\"3827\"])")], "")
.await
.with_status(StatusCode::PRECONDITION_FAILED);
let condition = format!("([{}])", etag);
client
.request_with_headers("DELETE", path, [("if", condition.as_str())], "")
.await
.with_status(StatusCode::NO_CONTENT);
client
.request("DELETE", path, "")
.await
.with_status(StatusCode::NOT_FOUND);
}
client.delete_default_containers().await;
mike_noquota.delete_default_containers().await;
test.assert_is_empty().await;
}