Skip to content

M12.1-4: ssh_config parser + resolver (private types only)#1

Merged
UnbreakableMJ merged 5 commits into
mainfrom
feature/m12-ssh-config-parser
May 3, 2026
Merged

M12.1-4: ssh_config parser + resolver (private types only)#1
UnbreakableMJ merged 5 commits into
mainfrom
feature/m12-ssh-config-parser

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

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 (IdentityFileVec, StrictHostKeyChecking enum, deprecation shims for old fields) lands separately as M12.5, which is also the cut for anvil-ssh 0.3.0.

What landed

  • M12.1ssh_config::lexer (line tokenizer, comment stripping, \ continuation, double-quoted runs with \"/\\ escapes, keyword=value) and ssh_config::parser (Host/Match/Global block grouping with negated-pattern recording).
  • M12.2ssh_config::include (recursive Include with tilde/env expansion, single-component glob, 16-deep limit, canonicalized-path cycle detection) and shared expand_tilde / expand_env / wildcard_match helpers in lexer.
  • M12.3ssh_config::matcher (host pattern matcher with * / ? globs, !-negation overriding positives on the same line, case-insensitive comparison, Match blocks silently skipped per PRD §12 Q1).
  • M12.4ssh_config::resolver with the public resolve(host, paths) -> ResolvedSshConfig entry 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 upcoming gitway diag config_source= field (NFR-24) and gitway config show.

Test surface

  • 143 lib unit tests (up from 124; 19 new resolver tests + 16 matcher + 13 include + 18 lexer-helper).
  • Acceptance-matrix harness at tests/ssh_config_acceptance.rs walks every YAML in tests/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_yml added as dev-dependencies only (never ship in the published crate).

Out of scope (next PRs)

  • M12.5AnvilConfig API break (identity_file: Optionidentity_files: Vec, skip_host_check: boolStrictHostKeyChecking enum, apply_ssh_config builder method). Cuts anvil-ssh 0.3.0.
  • M12.6log::warn! for parsed-but-not-honored algorithm directives (M17 plumbs them through to russh).
  • M12.7 — Gitway-side gitway config show <host> + --no-config flag, bumps anvil-ssh dep to 0.3.0, tags v1.0.0-rc.3.
  • M12.8 / M12.9 — NFR-24 diag wiring, NFR-15 latency bench, full directive matrix.
  • Match blocks — deferred to v1.1 per PRD §12 Q1; recognized at parse time for correct directive grouping but never match a host.

Test plan

  • CI passes (Linux/macOS/Windows + MSRV 1.88).
  • cargo fmt --all -- --check clean.
  • cargo clippy --all-targets --all-features -- -D warnings clean.
  • cargo test --lib --tests --locked green on all platforms.
  • cargo test --test ssh_config_acceptance green (matrix harness).

🤖 Generated with Claude Code

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).
@UnbreakableMJ
UnbreakableMJ merged commit d26f339 into main May 3, 2026
5 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feature/m12-ssh-config-parser branch May 3, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant