fix(lib): harden the distributed leaf against hostile and slow parents

Review of the leaf found the seams a stranger on the other end of a D
connection could pull at, and a few that a slow one would.

- A code-93 envelope is unwrapped once, as Nicotine+ does; a frame made
  of nothing but envelopes used to recurse once per byte.
- A branch level is validated before it is used: negative levels leave
  the candidate unadoptable and the largest one no longer overflows.
- The link reads through the crate's MessageReader, so a frame split
  across a read timeout is no longer torn or mistaken for a closed link.
- Every event a link raises carries its link number, so a stale thread
  cannot adopt, close or move a newer link to the same user.
- Later branch facts from the parent move our own place and are told to
  the server; a fresh login resets the leaf, whose stance is now the one
  place the four announcement messages come from.
- Candidates from a later batch join the ones in flight instead of
  cancelling them, capped at ten; searches from the parent are budgeted
  at fifty a second; a reply queued for a searcher the server cannot
  place expires after a minute.
- Truncated frames and half-read PossibleParents entries are dropped
  instead of yielding an empty root, a level of zero or a phantom
  candidate.

The e2e now has a searcher of its own, so the answer must go to whoever
asked and not to the parent that relayed it.
This commit is contained in:
Michel de Graaf
2026-09-08 01:45:07 +02:00
parent 7d7b8fe98c
commit 9d6a5aaa98
9 changed files with 589 additions and 207 deletions
@@ -193,18 +193,10 @@ fn a_successful_login_marks_the_session_live_again() {
#[test]
fn post_login_messages_carry_counts_and_conditional_wait_port() {
let messages = post_login_messages(true, 4321, 3, 7, "me");
let messages = post_login_messages(true, 4321, 3, 7);
let codes: Vec<u32> = messages.iter().map(code_of).collect();
// SharedFolders; the distributed stance HaveNoParent, BranchRoot,
// BranchLevel, AcceptChildren; SetStatus; SetWaitPort.
assert_eq!(codes, vec![35, 71, 127, 126, 100, 28, 2]);
// A leaf with no parent is its own branch root at level 0, and it
// declines children.
let mut root = messages[2].clone();
root.set_pointer(4);
assert_eq!(root.read_string(), "me");
assert_eq!(&messages[3].get_data()[4..8], &0u32.to_le_bytes());
assert_eq!(messages[4].get_data()[4], 0);
// SharedFolders; SetStatus; SetWaitPort.
assert_eq!(codes, vec![35, 28, 2]);
// The SharedFolders message (code 35) carries the real counts.
let shared = messages[0].get_data();
@@ -212,7 +204,7 @@ fn post_login_messages_carry_counts_and_conditional_wait_port() {
assert_eq!(u32::from_le_bytes(shared[8..12].try_into().unwrap()), 7);
// Not listening omits SetWaitPort (code 2).
let no_listen = post_login_messages(false, 4321, 3, 7, "me");
let no_listen = post_login_messages(false, 4321, 3, 7);
let codes: Vec<u32> = no_listen.iter().map(code_of).collect();
assert_eq!(codes, vec![35, 71, 127, 126, 100, 28]);
assert_eq!(codes, vec![35, 28]);
}