mirror of
https://github.com/Twigpine/node.git
synced 2026-11-08 07:38:18 -10:00
Merge remote-tracking branch 'origin/main' into fix/served-git-concurrency-cap
This commit is contained in:
commit
17f9edad47
4 files changed
+264
-16
No files matched your search
Generated
+81
-6
@@ -770,6 +770,23 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-ff"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af"
|
||||
dependencies = [
|
||||
"ark-ff-asm 0.6.0",
|
||||
"ark-ff-macros 0.6.0",
|
||||
"ark-serialize 0.6.0",
|
||||
"ark-std 0.6.0",
|
||||
"digest 0.10.7",
|
||||
"educe",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-ff-asm"
|
||||
version = "0.3.0"
|
||||
@@ -800,6 +817,16 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-ff-asm"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-ff-macros"
|
||||
version = "0.3.0"
|
||||
@@ -838,6 +865,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-ff-macros"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-serialize"
|
||||
version = "0.3.0"
|
||||
@@ -871,6 +911,30 @@ dependencies = [
|
||||
"num-bigint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-serialize"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b"
|
||||
dependencies = [
|
||||
"ark-serialize-derive",
|
||||
"ark-std 0.6.0",
|
||||
"digest 0.10.7",
|
||||
"num-bigint",
|
||||
"serde_with",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-serialize-derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-std"
|
||||
version = "0.3.0"
|
||||
@@ -901,6 +965,16 @@ dependencies = [
|
||||
"rand 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ark-std"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.8.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -2556,7 +2630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2928,7 +3002,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5741,14 +5815,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruint"
|
||||
version = "1.17.2"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a"
|
||||
checksum = "f5e99bff0393163bb25029a6af25d3d8d202ba5b5438a74d1bd8789f5c822970"
|
||||
dependencies = [
|
||||
"alloy-rlp",
|
||||
"ark-ff 0.3.0",
|
||||
"ark-ff 0.4.2",
|
||||
"ark-ff 0.5.0",
|
||||
"ark-ff 0.6.0",
|
||||
"bytes",
|
||||
"fastrlp 0.3.1",
|
||||
"fastrlp 0.4.0",
|
||||
@@ -5822,7 +5897,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6716,7 +6791,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -917,6 +917,10 @@ pub(crate) fn normalize_owner_key(did: &str) -> &str {
|
||||
/// drift apart. If you change `normalize_owner_key`, update this const too.
|
||||
const OWNER_KEY_CASE_SQL: &str = "CASE WHEN owner_did LIKE 'did:key:%' AND position(':' in substr(owner_did, 9)) = 0 THEN substr(owner_did, 9) ELSE owner_did END";
|
||||
|
||||
/// SQL CASE expression byte-identical to `normalize_owner_key`, but for columns
|
||||
/// named `did` (like in agent_profiles) instead of `owner_did`.
|
||||
const PROFILE_DID_CASE_SQL: &str = "CASE WHEN did LIKE 'did:key:%' AND position(':' in substr(did, 9)) = 0 THEN substr(did, 9) ELSE did END";
|
||||
|
||||
#[cfg(test)]
|
||||
mod normalize_owner_key_tests {
|
||||
use super::normalize_owner_key;
|
||||
@@ -3454,6 +3458,9 @@ impl Db {
|
||||
let new_website = website.or(existing.website.as_deref());
|
||||
let new_socials = socials.or(existing.socials.as_deref());
|
||||
|
||||
// get_profile equates bare short ids with did:key:<id>. UPDATE must target
|
||||
// the stored row identity (existing.did), not the caller's raw input form,
|
||||
// or a did:key: alias against a bare-stored profile updates zero rows.
|
||||
sqlx::query(
|
||||
"UPDATE agent_profiles
|
||||
SET display_name=$1, bio=$2, avatar_url=$3, website=$4, socials=$5, updated_at=$6
|
||||
@@ -3465,12 +3472,12 @@ impl Db {
|
||||
.bind(new_website)
|
||||
.bind(new_socials)
|
||||
.bind(&now)
|
||||
.bind(did)
|
||||
.bind(&existing.did)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(ProfileRecord {
|
||||
did: did.to_string(),
|
||||
did: existing.did,
|
||||
display_name: new_name.map(String::from),
|
||||
bio: new_bio.map(String::from),
|
||||
avatar_url: new_avatar.map(String::from),
|
||||
@@ -3511,14 +3518,20 @@ impl Db {
|
||||
}
|
||||
|
||||
pub async fn get_profile(&self, did: &str) -> Result<Option<ProfileRecord>> {
|
||||
let row = sqlx::query(
|
||||
// Same owner-key contract as get_repo: strip `did:key:` only when the
|
||||
// remainder is a bare key id. The old `LIKE '%:' || $1` matched any DID
|
||||
// method that shared a suffix and could resolve the wrong profile.
|
||||
let did_key = normalize_owner_key(did);
|
||||
let sql = format!(
|
||||
"SELECT did, display_name, bio, avatar_url, website, socials, profile_cid, created_at, updated_at
|
||||
FROM agent_profiles
|
||||
WHERE did = $1 OR did LIKE '%:' || $1",
|
||||
)
|
||||
.bind(did)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
WHERE ({key}) = $1",
|
||||
key = PROFILE_DID_CASE_SQL
|
||||
);
|
||||
let row = sqlx::query(&sql)
|
||||
.bind(did_key)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|r| ProfileRecord {
|
||||
did: r.get("did"),
|
||||
@@ -3534,10 +3547,17 @@ impl Db {
|
||||
}
|
||||
|
||||
pub async fn set_profile_cid(&self, did: &str, cid: &str) -> Result<()> {
|
||||
sqlx::query("UPDATE agent_profiles SET profile_cid = $1, updated_at = $2 WHERE did = $3")
|
||||
// Same did:key / bare equivalence as get_profile so a full did:key: form
|
||||
// updates a profile stored under the bare short id.
|
||||
let did_key = normalize_owner_key(did);
|
||||
let sql = format!(
|
||||
"UPDATE agent_profiles SET profile_cid = $1, updated_at = $2 WHERE ({key}) = $3",
|
||||
key = PROFILE_DID_CASE_SQL
|
||||
);
|
||||
sqlx::query(&sql)
|
||||
.bind(cid)
|
||||
.bind(Utc::now().to_rfc3339())
|
||||
.bind(did)
|
||||
.bind(did_key)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -4671,6 +4691,103 @@ mod dedup_db_tests {
|
||||
assert!(!got.is_public, "non-key row's is_public must be preserved");
|
||||
}
|
||||
|
||||
/// get_profile must not resolve a non-key DID (e.g. did:gitlawb:) when
|
||||
/// queried with the bare short id. The old `LIKE '%:' || $1` clause was too
|
||||
/// broad and could return the wrong profile row.
|
||||
#[sqlx::test]
|
||||
async fn get_profile_does_not_match_non_key_did(pool: PgPool) {
|
||||
let db = db(pool).await;
|
||||
let short = "z6Mkprof1";
|
||||
|
||||
// Seed only a non-key DID row first. When queried with the bare short ID,
|
||||
// get_profile must return None (lone non-key fixture test).
|
||||
db.upsert_profile(
|
||||
&format!("did:gitlawb:{short}"),
|
||||
Some("other-method"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let got = db.get_profile(short).await.unwrap();
|
||||
assert!(
|
||||
got.is_none(),
|
||||
"bare short id must not resolve a lone non-key DID profile"
|
||||
);
|
||||
|
||||
// Now seed the canonical bare key profile row as well.
|
||||
db.upsert_profile(short, Some("canonical"), None, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let got = db
|
||||
.get_profile(short)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("bare short id should resolve the key-form profile");
|
||||
assert_eq!(got.did, short);
|
||||
assert_eq!(got.display_name.as_deref(), Some("canonical"));
|
||||
|
||||
let got = db
|
||||
.get_profile(&format!("did:key:{short}"))
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("did:key form should also resolve the key-form profile");
|
||||
assert_eq!(got.did, short);
|
||||
|
||||
let got = db
|
||||
.get_profile(&format!("did:gitlawb:{short}"))
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("full non-key DID should resolve its own profile");
|
||||
assert_eq!(got.did, format!("did:gitlawb:{short}"));
|
||||
assert_eq!(got.display_name.as_deref(), Some("other-method"));
|
||||
}
|
||||
|
||||
/// upsert_profile must update a bare-stored profile when called with the
|
||||
/// full did:key: form. get_profile equates the two, so the UPDATE has to
|
||||
/// target existing.did rather than the raw input.
|
||||
#[sqlx::test]
|
||||
async fn upsert_profile_updates_via_did_key_alias(pool: PgPool) {
|
||||
let db = db(pool).await;
|
||||
let short = "z6Mkprof2";
|
||||
|
||||
db.upsert_profile(short, Some("before"), None, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = db
|
||||
.upsert_profile(
|
||||
&format!("did:key:{short}"),
|
||||
Some("after"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.did, short, "preserve the stored did form on update");
|
||||
assert_eq!(updated.display_name.as_deref(), Some("after"));
|
||||
|
||||
let got = db
|
||||
.get_profile(short)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("profile should still resolve by bare short id");
|
||||
assert_eq!(got.did, short);
|
||||
assert_eq!(got.display_name.as_deref(), Some("after"));
|
||||
|
||||
db.set_profile_cid(&format!("did:key:{short}"), "bafytestcid")
|
||||
.await
|
||||
.unwrap();
|
||||
let got = db.get_profile(short).await.unwrap().unwrap();
|
||||
assert_eq!(got.profile_cid.as_deref(), Some("bafytestcid"));
|
||||
}
|
||||
|
||||
/// Verify that the Rust `normalize_owner_key` and the `OWNER_KEY_CASE_SQL`
|
||||
/// expression agree on every boundary value in the owner-key normalization
|
||||
/// set. A mismatch would let the Rust code bind a different key than the SQL
|
||||
@@ -4720,6 +4837,49 @@ mod dedup_db_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that `PROFILE_DID_CASE_SQL` (which aliases the column `did`) also
|
||||
/// agrees with Rust `normalize_owner_key` across the full boundary matrix.
|
||||
#[sqlx::test]
|
||||
async fn profile_did_case_sql_matches_normalize_owner_key(pool: PgPool) {
|
||||
let boundary_values = [
|
||||
"did:key:z6Mkfoo",
|
||||
"z6Mkfoo",
|
||||
"did:gitlawb:z6Mkfoo",
|
||||
"did:web:example.com:alice",
|
||||
"did:key:did:gitlawb:z6Mkfoo",
|
||||
"",
|
||||
"did:key:",
|
||||
"DID:KEY:z6Mkfoo",
|
||||
];
|
||||
|
||||
let values_sql: String = boundary_values
|
||||
.iter()
|
||||
.map(|v| format!("('{}'::text)", v))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let sql = format!(
|
||||
"WITH data(did) AS (VALUES {values_sql})
|
||||
SELECT did, ({key}) AS normalized FROM data ORDER BY did",
|
||||
key = super::PROFILE_DID_CASE_SQL
|
||||
);
|
||||
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(&sql).fetch_all(&pool).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
rows.len(),
|
||||
boundary_values.len(),
|
||||
"every boundary value must produce a row"
|
||||
);
|
||||
|
||||
for (val, sql_result) in &rows {
|
||||
let rust_result = super::normalize_owner_key(val);
|
||||
assert_eq!(
|
||||
sql_result, rust_result,
|
||||
"PROFILE_DID_CASE_SQL(\"{val}\") mismatch: Rust = \"{rust_result}\", SQL CASE = \"{sql_result}\""
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercises the iCaptcha single-use proof ledger (`icaptcha_consumed_proofs`),
|
||||
|
||||
@@ -880,6 +880,7 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn containment_rejects_a_symlinked_directory_inside_the_root() {
|
||||
use std::os::unix::fs::symlink;
|
||||
@@ -896,6 +897,7 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn containment_rejects_a_symlinked_file_inside_the_root() {
|
||||
use std::os::unix::fs::symlink;
|
||||
@@ -927,6 +929,7 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn containment_rejects_a_missing_candidate_under_a_symlinked_parent() {
|
||||
use std::os::unix::fs::symlink;
|
||||
@@ -943,6 +946,7 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn containment_reports_io_error_for_a_dangling_symlink() {
|
||||
// The link entry exists, so the candidate is the thing to resolve, and
|
||||
|
||||
@@ -1406,6 +1406,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn process_batch_rejects_a_symlinked_owner_directory(pool: PgPool) {
|
||||
// The slug is textually valid, so U1's character rules pass it. The
|
||||
@@ -1438,6 +1439,7 @@ mod tests {
|
||||
assert_eq!(sync_status(&pool, "z6Mkfoo/hello").await, "failed");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn process_batch_rejects_a_symlinked_mirror_path(pool: PgPool) {
|
||||
// The leaf case a parent-only canonicalize misses: the owner directory
|
||||
@@ -1561,6 +1563,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn process_batch_leaves_the_row_pending_when_the_mirror_cannot_be_inspected(
|
||||
pool: PgPool,
|
||||
@@ -1604,6 +1607,7 @@ mod tests {
|
||||
assert!(owner_dir.join("hello.git").is_dir());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn process_batch_leaves_the_row_pending_when_the_owner_dir_cannot_be_created(
|
||||
pool: PgPool,
|
||||
@@ -1680,6 +1684,7 @@ mod tests {
|
||||
assert!(mirrors_under(home.path()).is_empty());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn process_batch_terminally_fails_when_a_dangling_symlink_occupies_the_owner_path(
|
||||
pool: PgPool,
|
||||
@@ -1732,6 +1737,7 @@ mod tests {
|
||||
/// only needs +x on repos_dir), so this defers at `path_within_root`
|
||||
/// instead — the stall path that survives the AlreadyExists
|
||||
/// classification, which is what keeps the starvation tests load-bearing.
|
||||
#[cfg(unix)]
|
||||
fn make_stuck_owner(repos_dir: &Path, owner: &str) -> std::path::PathBuf {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let dir = repos_dir.join(owner);
|
||||
@@ -1752,11 +1758,13 @@ mod tests {
|
||||
dir
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn unstick(dir: &Path) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn a_full_batch_of_stuck_rows_does_not_starve_a_healthy_one(pool: PgPool) {
|
||||
// Ten rows that defer forever, exactly filling the batch, with one
|
||||
@@ -1811,6 +1819,7 @@ mod tests {
|
||||
unstick(&stuck);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[sqlx::test]
|
||||
async fn a_stuck_set_larger_than_the_batch_still_yields(pool: PgPool) {
|
||||
// 25 stuck rows against a batch size of 10. The healthy row lands
|
||||
|
||||
Reference in new issue
Block a user