M12.1-4: ssh_config parser + resolver (private types only)#1
Merged
Conversation
Adds the first slice of the ssh_config(5) parser landing as anvil-ssh 0.3.0 per Gitway PRD §5.8.1 / FR-47..54 / M12. - ssh_config::lexer: line-oriented tokenizer with comment stripping, backslash line-continuation joining, double-quoted runs (with escape pairs), and the keyword=value form. Lower-cases keywords; preserves argument case. Emits TokenLine with file path + 1-based line number for resolver-stage provenance (NFR-24). - ssh_config::parser: groups TokenLines into ordered HostBlocks (Global / Host(patterns) / Match). Records pattern negation for `Host !work *`. Match blocks are recognized for correct directive grouping but never match a real host — full Match semantics deferred to v1.1 per PRD §12 Q1. Both submodules are pub(crate) and wrapped in a module-level #[allow(dead_code)]; M12.4 wires up the public resolve() entry point and removes the allow. Tests: 16 lexer + 8 parser unit tests covering the OpenSSH lexing / grouping edge cases (continuation, CRLF, escaped quotes, # inside quotes, multi-pattern Host, Match-block grouping, malformed input). All 77 lib tests pass; 0 failed; 1 ignored (pre-existing). Plan: M12.1 of let-us-plan-on-bright-cosmos.md.
…cle detection
Adds the second slice of the ssh_config(5) parser landing as anvil-ssh
0.3.0 per Gitway PRD §5.8.1 / FR-47..54 / M12.
lexer.rs (additions):
- expand_tilde: leading `~/` or `~` -> dirs::home_dir(). `~user/` is
preserved verbatim with a warning, matching Windows OpenSSH behavior
across all platforms.
- expand_env: substitutes `${VAR}` and `$VAR`. Unknown variables expand
to the empty string (POSIX/OpenSSH semantics). Bare `$`, `$<digit>`,
and unterminated `${...` are preserved verbatim.
- wildcard_match: shell-style `*`/`?` matching used by both the include
glob expander here and (in M12.3) the host-pattern matcher.
ssh_config::include (new):
- expand_includes: recursively inlines Included file contents in token
order. Composition: env-expand -> tilde-expand -> filesystem glob on
the final path component. Multi-component globs are rejected with a
clear error (avoids reimplementing fnmatch(3)'s quirks).
- 16-deep nesting limit (matches OpenSSH's READCONF_MAX_DEPTH).
- Cycle detection by canonicalized path; the primary file is seeded
into `visited`. Sibling Includes don't false-positive (path is popped
after each branch); diamond imports process the target twice
(matches OpenSSH; not memoized).
- Missing literal-path Includes and zero-match globs are silent no-ops,
matching OpenSSH.
- Component-walking-based wildcard detection skips the Windows `\\?\`
extended-length prefix (which contains a literal `?`).
Tests: 31 new (lexer 18, include 13). Total now 108 passing, 0 failed.
Edge cases covered: relative-path resolution, glob alphabetization,
multi-path Include lines, depth-first inlining, diamond imports,
self-include cycle, mutual cycle, sibling non-cycle, multi-component
glob rejection, provenance preservation across Include boundary.
Plan: M12.2 of let-us-plan-on-bright-cosmos.md.
Adds the third slice of the ssh_config(5) parser landing as anvil-ssh 0.3.0 per Gitway PRD §5.8.1 / FR-47..54 / M12. ssh_config::matcher (new): - directives_for_host: walks the parsed block list once and returns every directive whose containing block matches the host, in source order. The Global section always applies; Host blocks apply per the pattern rules below; Match blocks are silently skipped (deferred to v1.1 per PRD §12 Q1). - host_block_matches: a Host block applies when at least one positive pattern matches AND no negated pattern matches. Per ssh_config(5), any negated match overrides every other pattern on the line and ignores the entire Host entry. - Hostname comparisons are case-insensitive (matches DNS rules and OpenSSH match_pattern, which lower-cases both sides). - Glob syntax shares the wildcard_match helper added in M12.2 (* for zero-or-more, ? for exactly-one). Tests: 16 new matcher tests covering exact match, * and ? wildcards, multi-pattern Host lines, negation override, only-negated no-op, case insensitivity (including with wildcards), source-order concatenation across multiple matching blocks, Match-block skip, provenance preservation. Total now 124 passing, 0 failed. Plan: M12.3 of let-us-plan-on-bright-cosmos.md.
…ix harness Wires the M12.1-M12.3 building blocks together into the public API landing as anvil-ssh 0.3.0 per Gitway PRD §5.8.1 / FR-47..54 / M12. ssh_config::resolver (new): - pub fn resolve(host, paths) -> ResolvedSshConfig: composes the lexer, include resolver, parser, and host matcher into one call. Reads user file then system file (per ssh_config(5): "first obtained value for each parameter is used"). Missing files are silently skipped. - ResolvedSshConfig: holds every directive supported by §5.8.1 — HostName, User, Port, IdentityFile (Vec), IdentitiesOnly, IdentityAgent, CertificateFile (Vec), ProxyCommand, ProxyJump, UserKnownHostsFile (Vec), StrictHostKeyChecking, HostKeyAlgorithms, KexAlgorithms, Ciphers, MACs, ConnectTimeout (Duration), ConnectionAttempts. First-occurrence-wins for single-valued fields; multi-valued fields accumulate every occurrence. - SshConfigPaths: explicit user/system path knobs with platform defaults (Unix /etc/ssh/ssh_config; Windows %PROGRAMDATA%\ssh\ ssh_config; user always ~/.ssh/config). - StrictHostKeyChecking enum (Yes/No/AcceptNew). `ask` folds into Yes since this crate never prompts. - AlgList newtype around the raw algorithm spec; M17 will plumb the +/-/^ modifier semantics through to russh. - DirectiveSource: provenance entry (file + line) per applied directive for the gitway diag config_source= field (NFR-24) and config show. - Tilde + env expansion applied to all path-shaped directive values. - Unknown directives silently skipped (trace-level log) so that ssh_config(5) directives outside §5.8.1 don't error out. lib.rs / mod.rs: - ssh_config promoted from crate-private mod to pub mod. - Sub-modules (lexer, parser, include, matcher, resolver) remain pub(crate); only the resolver's API is re-exported. - Crate-root re-exports for AlgList, DirectiveSource, ResolvedSshConfig, SshConfigPaths, StrictHostKeyChecking. resolve() stays at ssh_config::resolve to avoid polluting the top-level namespace with a generic name. - Removes the M12.1-era #![allow(dead_code)] from ssh_config/mod.rs; every helper is now consumed by the resolver. Acceptance matrix scaffolding: - tests/ssh_config_acceptance.rs walks tests/ssh_config_matrix/*.yaml and asserts that resolved fields match the expected values declared in each fixture. Covers basic Host blocks, first-wins precedence, and negation. M12.9 expands the matrix to every directive in §5.8.1. - serde + serde_yml added as dev-dependencies only — never ship in the published crate. serde_yml is the maintained successor to the deprecated serde_yaml. Tests: 19 new resolver unit tests + 1 matrix harness test. Total now 143 lib tests + 1 matrix test, 0 failures. Verifies basic resolution, first-wins, multi-IdentityFile (multi-line and multi-arg-per-line), StrictHostKeyChecking variants, algorithm directive raw capture, ConnectTimeout Duration mapping, ProxyCommand re-join, provenance preservation, user-then-system precedence, missing-file silence. Plan: M12.4 of let-us-plan-on-bright-cosmos.md. Closes the M12.1-4 PR scope (the four building-block sub-milestones).
This was referenced May 3, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
ssh_config(5)parser and resolver to anvil-ssh as four reviewable sub-milestones (M12.1 → M12.4 of the M12 plan). All commits are on this branch; each is independently buildable and tested.This is the building-block PR — the public API surfaces here as additive new types. No existing items break. The semver-minor API break (
IdentityFile→Vec,StrictHostKeyCheckingenum, deprecation shims for old fields) lands separately as M12.5, which is also the cut for anvil-ssh 0.3.0.What landed
ssh_config::lexer(line tokenizer, comment stripping,\continuation, double-quoted runs with\"/\\escapes,keyword=value) andssh_config::parser(Host/Match/Global block grouping with negated-pattern recording).ssh_config::include(recursiveIncludewith tilde/env expansion, single-component glob, 16-deep limit, canonicalized-path cycle detection) and sharedexpand_tilde/expand_env/wildcard_matchhelpers in lexer.ssh_config::matcher(host pattern matcher with*/?globs,!-negation overriding positives on the same line, case-insensitive comparison,Matchblocks silently skipped per PRD §12 Q1).ssh_config::resolverwith the publicresolve(host, paths) -> ResolvedSshConfigentry point. Handles every directive in PRD §5.8.1:HostName,User,Port,IdentityFile(multi),IdentitiesOnly,IdentityAgent,CertificateFile(multi),ProxyCommand,ProxyJump,UserKnownHostsFile(multi),StrictHostKeyChecking,HostKeyAlgorithms,KexAlgorithms,Ciphers,MACs,ConnectTimeout,ConnectionAttempts. Per-directive provenance preserved for the upcominggitway diag config_source=field (NFR-24) andgitway config show.Test surface
tests/ssh_config_acceptance.rswalks every YAML intests/ssh_config_matrix/. Three seed fixtures (basic block, first-wins precedence, negation). M12.9 expands the matrix to cover every directive in §5.8.1.serde+serde_ymladded as dev-dependencies only (never ship in the published crate).Out of scope (next PRs)
AnvilConfigAPI break (identity_file: Option→identity_files: Vec,skip_host_check: bool→StrictHostKeyCheckingenum,apply_ssh_configbuilder method). Cuts anvil-ssh 0.3.0.log::warn!for parsed-but-not-honored algorithm directives (M17 plumbs them through to russh).gitway config show <host>+--no-configflag, bumps anvil-ssh dep to 0.3.0, tagsv1.0.0-rc.3.Matchblocks — deferred to v1.1 per PRD §12 Q1; recognized at parse time for correct directive grouping but never match a host.Test plan
cargo fmt --all -- --checkclean.cargo clippy --all-targets --all-features -- -D warningsclean.cargo test --lib --tests --lockedgreen on all platforms.cargo test --test ssh_config_acceptancegreen (matrix harness).🤖 Generated with Claude Code