Skip to content

Commit d26f339

Browse files
M12.1-4: ssh_config parser + resolver (private types only) (#1)
* feat(ssh_config): M12.1 — lexer + parser skeleton (private) 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. * feat(ssh_config): M12.2 — Include resolver + tilde/env expansion + cycle 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. * feat(ssh_config): M12.3 — host pattern matcher (globs + negation) 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. * feat(ssh_config): M12.4 — public resolve() + ResolvedSshConfig + matrix 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). * fix(ssh_config): rustfmt import-ordering nit (case-insensitive sort)
1 parent 89a42d2 commit d26f339

13 files changed

Lines changed: 2555 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ nix = { version = "0.29", features = ["fs", "process", "signal", "user"] }
5151
tokio = { version = "1", features = ["io-util", "io-std", "rt-multi-thread", "net", "sync", "macros", "signal", "time", "process"] }
5252
tempfile = "3"
5353
criterion = { version = "0.5", features = ["async_tokio"] }
54+
# Acceptance-matrix fixtures for the ssh_config parser are YAML;
55+
# `serde_yml` is the maintained successor to `serde_yaml`. Dev-dep only —
56+
# never ships in the published crate's runtime tree.
57+
serde = { version = "1", features = ["derive"] }
58+
serde_yml = "0.0.12"
5459

5560
[[bench]]
5661
name = "throughput"

src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,19 @@ pub mod session;
5555
pub mod sshsig;
5656
pub mod time;
5757

58+
// `ssh_config(5)` parser and resolver. Public API is re-exported below;
59+
// the sub-modules (lexer, parser, include, matcher, resolver) themselves
60+
// are crate-private.
61+
pub mod ssh_config;
62+
5863
// ── Flat re-exports (FR-23) ───────────────────────────────────────────────────
5964

6065
pub use config::AnvilConfig;
6166
pub use error::AnvilError;
6267
pub use session::AnvilSession;
68+
pub use ssh_config::{
69+
AlgList, DirectiveSource, ResolvedSshConfig, SshConfigPaths, StrictHostKeyChecking,
70+
};
6371

6472
// ── Deprecated 0.1.x compatibility aliases ────────────────────────────────────
6573
//

0 commit comments

Comments
 (0)