Fixed DHT republish after TTL

This commit is contained in:
Ultradesu
2026-07-20 14:22:15 +03:00
parent 3f15e9aebd
commit fdbc0e2ad0
3 changed files with 280 additions and 192 deletions
+20 -4
View File
@@ -84,7 +84,7 @@ pub trait MusicDhtStorage: std::fmt::Debug + Send + Sync {
}
/// Searches locally owned active items: exact normalized match, or all
/// query tokens present in the item's token set.
/// query tokens present in the item's token set, including artist names.
async fn search_local(&self, normalized_query: String) -> Result<Vec<LibraryItem>> {
let all = self.list_local_items(false).await?;
let query_tokens = tokenize(&normalized_query);
@@ -97,7 +97,7 @@ pub trait MusicDhtStorage: std::fmt::Debug + Send + Sync {
if query_tokens.is_empty() {
return false;
}
let artist_tokens = tokenize(&item.normalized_name);
let artist_tokens = item.search_tokens();
query_tokens
.iter()
.all(|token| artist_tokens.iter().any(|t| t == token))
@@ -237,7 +237,7 @@ impl Database {
}
/// Searches locally owned active items: exact normalized match, or all
/// query tokens present in the item's token set.
/// query tokens present in the item's token set, including artist names.
pub async fn search_local(&self, normalized_query: String) -> Result<Vec<LibraryItem>> {
let all = self.list_local_items(false).await?;
let query_tokens = tokenize(&normalized_query);
@@ -250,7 +250,7 @@ impl Database {
if query_tokens.is_empty() {
return false;
}
let artist_tokens = tokenize(&item.normalized_name);
let artist_tokens = item.search_tokens();
query_tokens
.iter()
.all(|token| artist_tokens.iter().any(|t| t == token))
@@ -522,6 +522,22 @@ mod tests {
assert!(none.is_empty());
}
#[tokio::test]
async fn local_search_uses_artist_names() {
let (_dir, db) = open_temp().await;
let owner = test_peer(1);
let mut track = item(owner, "Teardrop", 1, false);
track.kind = ItemKind::Track;
track.artist_names = vec!["Massive Attack".into()];
db.upsert_local_item(&track).await.expect("upsert");
let found = db
.search_local("massive attack".into())
.await
.expect("search");
assert_eq!(found, vec![track]);
}
#[tokio::test]
async fn expired_dht_record_is_not_returned() {
let (_dir, db) = open_temp().await;
+70 -6
View File
@@ -5,8 +5,8 @@ use federation_net::NetworkId;
use crate::message::StoreRecordRequest;
use crate::normalization::{normalize_name, tokenize};
use crate::record::{
ACTIVE_RECORD_TTL, DhtKey, MAX_ITEM_NAME_BYTES, MAX_TOKENS_PER_ITEM, StoredRecord,
TOMBSTONE_TTL,
ACTIVE_RECORD_TTL, DhtKey, MAX_ARTISTS_PER_ITEM, MAX_ITEM_NAME_BYTES, MAX_TOKENS_PER_ITEM,
StoredRecord, TOMBSTONE_TTL,
};
/// Outcome of comparing an incoming record with the stored one.
@@ -59,9 +59,9 @@ pub fn decide_store(existing: Option<(u64, bool, u64)>, incoming: &StoredRecord)
/// Validates an incoming `StoreRecord` request.
///
/// Checks the size limits, that the record is internally consistent, that the
/// key actually corresponds to the record's name or one of its tokens, and
/// clamps the expiry to the maximum TTL allowed for the record type. Returns
/// the record with a possibly clamped `expires_at_ms`.
/// key actually corresponds to the record's name, or one of the item's/artist's
/// search tokens, and clamps the expiry to the maximum TTL allowed for the
/// record type. Returns the record with a possibly clamped `expires_at_ms`.
pub(crate) fn validate_store(
request: StoreRecordRequest,
network_id: &NetworkId,
@@ -83,9 +83,21 @@ pub(crate) fn validate_store(
if tokens.len() > MAX_TOKENS_PER_ITEM {
return Err("too many tokens".into());
}
if item.artist_names.len() > MAX_ARTISTS_PER_ITEM {
return Err("too many artists".into());
}
for artist in &item.artist_names {
if artist.len() > MAX_ITEM_NAME_BYTES {
return Err("artist name too long".into());
}
if tokenize(&normalize_name(artist)).len() > MAX_TOKENS_PER_ITEM {
return Err("artist name has too many tokens".into());
}
}
let key_matches = request.key == DhtKey::exact(network_id, &item.normalized_name)
|| tokens
|| item
.search_tokens()
.iter()
.any(|token| request.key == DhtKey::token(network_id, token));
if !key_matches {
@@ -138,6 +150,28 @@ mod tests {
}
}
fn track_with_artist(expires_at_ms: u64) -> StoredRecord {
let owner = test_peer(1);
StoredRecord {
item: LibraryItem {
id: ItemId::from_bytes([8u8; 32]),
owner,
kind: crate::record::ItemKind::Track,
name: "Teardrop".into(),
normalized_name: "teardrop".into(),
artist_names: vec!["Massive Attack".into()],
year: Some(1998),
release_type: None,
duration_seconds: Some(330.0),
revision: 1,
deleted: false,
updated_at_ms: 0,
},
publisher: owner,
expires_at_ms,
}
}
#[test]
fn newer_revision_replaces_older() {
let incoming = record(2, false, 1000);
@@ -273,4 +307,34 @@ mod tests {
.is_err()
);
}
#[test]
fn validate_accepts_artist_token_keys() {
let net = NetworkId::from_name("test");
let now = 1_000_000;
let rec = track_with_artist(now + ACTIVE_RECORD_TTL.as_millis() as u64);
assert!(
validate_store(
StoreRecordRequest {
key: DhtKey::token(&net, "massive"),
record: rec.clone(),
},
&net,
now,
)
.is_ok()
);
assert!(
validate_store(
StoreRecordRequest {
key: DhtKey::token(&net, "attack"),
record: rec,
},
&net,
now,
)
.is_ok()
);
}
}