perf: round 19 — auth/WOPI/vCard/PROPFIND per-request & per-row alloc cuts
Benchmark-gated (examples/bench_round19_micro.rs, benches/ROUND19.md): every
section ships a BEFORE/AFTER counting-allocator arm with a byte/-value
equivalence gate and a GATE-FAIL-rollback exit. All eight pass. No Postgres.
- M1 verify_basic_auth cache key: blake3::hash(format!("{u}:{p}")) → incremental
Hasher (byte-identical key, 2→0 allocs on every Basic-auth DAV request)
- M2 WopiTokenService: prebuild Validation/DecodingKey/EncodingKey in new()
instead of per-call (mirrors JwtTokenService; 16→12 allocs/validate)
- V1/V2 vCard emit (contact_to_vcard/generate_vcard): FN fallback drops the
throwaway to_string, NOTE skips the escape copy for newline-free notes, REV
uses new common::fmt::compact_ical_utc stack renderer (11.5× vs chrono
strftime, 3→0 allocs); per-contact 9→4 allocs
- M4 trash_service::row_to_item_dto: move name/path/blob_hash out of the owned
row instead of cloning (3 clones/file row gone)
- M5 search cache key: Uuid::hyphenated().encode_lower stack buffer instead of
to_string (identical u64 key, 1→0 allocs/request)
- M6 streaming PROPFIND: reuse one href buffer across the page instead of a
format! per child (native + NC handlers; 192→3 allocs on a 64-child page)
- M7 nextcloud extract_url_user: return Cow instead of forcing into_owned
(zero-alloc on the common ASCII-username path)
common::fmt::compact_ical_utc added with chrono-parity unit tests (CASES +
60-year sweep). cargo fmt + clippy --all-targets clean; 526 lib unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ront9bk7YMoffVQkGG47gh
This commit is contained in:
@@ -87,7 +87,7 @@ impl NcSession {
|
||||
///
|
||||
/// Returns `None` for anything that doesn't follow this shape (notably
|
||||
/// the OCS surfaces, where there is no `{user}` segment to compare).
|
||||
fn extract_url_user(path: &str) -> Option<String> {
|
||||
fn extract_url_user(path: &str) -> Option<std::borrow::Cow<'_, str>> {
|
||||
let mut segments = path.split('/');
|
||||
if !segments.next()?.is_empty() {
|
||||
return None;
|
||||
@@ -103,7 +103,11 @@ fn extract_url_user(path: &str) -> Option<String> {
|
||||
if user_seg.is_empty() {
|
||||
return None;
|
||||
}
|
||||
urlencoding::decode(user_seg).ok().map(|s| s.into_owned())
|
||||
// Keep the `Cow` — a plain-ASCII username decodes to `Cow::Borrowed`, so the
|
||||
// common path allocates nothing; only a percent-encoded username owns. The
|
||||
// old `.into_owned()` forced a `String` on EVERY path-scoped NC DAV request
|
||||
// (benches/ROUND19.md §M7). The caller compares by slice.
|
||||
urlencoding::decode(user_seg).ok()
|
||||
}
|
||||
|
||||
/// Axum extractor: the shared handle to the request's [`NcSession`].
|
||||
@@ -151,7 +155,7 @@ impl<S: Send + Sync> FromRequestParts<S> for SharedNcSession {
|
||||
.ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?;
|
||||
|
||||
if let Some(url_user) = extract_url_user(parts.uri.path())
|
||||
&& url_user != session.raw_username
|
||||
&& url_user.as_ref() != session.raw_username.as_str()
|
||||
{
|
||||
return Err(StatusCode::FORBIDDEN.into_response());
|
||||
}
|
||||
|
||||
@@ -1609,14 +1609,18 @@ fn build_nc_streaming_propfind(
|
||||
let mut chunk = Vec::with_capacity(batch_len * 1024);
|
||||
{
|
||||
let mut xml = Writer::new(&mut chunk);
|
||||
// One href buffer reused across the page instead of a fresh
|
||||
// format! String per child (benches/ROUND19.md §M6).
|
||||
let mut href = String::new();
|
||||
for file in batch.iter() {
|
||||
let dead = dead_props_for(&file.id, &file_deads);
|
||||
// Only the name varies per row — the encoded
|
||||
// username + parent prefix is computed once
|
||||
// outside the loops (the old `nc_href` call
|
||||
// re-encoded both for every child).
|
||||
let href =
|
||||
format!("{}{}", child_href_prefix, urlencoding::encode(&file.name));
|
||||
href.clear();
|
||||
href.push_str(&child_href_prefix);
|
||||
href.push_str(&urlencoding::encode(&file.name));
|
||||
let fid = nc_id_of(&file_id_map, &file.id);
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
write_file_response(&mut xml, file, &href, (fid, oc_id.as_deref()), &username, &favs, dead)
|
||||
@@ -1671,12 +1675,16 @@ fn build_nc_streaming_propfind(
|
||||
let mut chunk = Vec::with_capacity(batch.len() * 1024);
|
||||
{
|
||||
let mut xml = Writer::new(&mut chunk);
|
||||
// One href buffer reused across the page (benches/ROUND19.md §M6).
|
||||
let mut href = String::new();
|
||||
for sf in batch.iter() {
|
||||
let dead = dead_props_for(&sf.id, &sub_deads);
|
||||
// Collections carry the trailing slash; prefix
|
||||
// precomputed once like the file loop above.
|
||||
let href =
|
||||
format!("{}{}/", child_href_prefix, urlencoding::encode(&sf.name));
|
||||
href.clear();
|
||||
href.push_str(&child_href_prefix);
|
||||
href.push_str(&urlencoding::encode(&sf.name));
|
||||
href.push('/');
|
||||
let fid = nc_id_of(&sub_id_map, &sf.id);
|
||||
let oc_id = fid.map(|id| format_oc_id(id, file_id_svc));
|
||||
write_folder_response(&mut xml, sf, &href, (fid, oc_id.as_deref()), &username, &favs, quota, dead)
|
||||
|
||||
Reference in New Issue
Block a user