Commit d26f339
authored
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
- src
- ssh_config
- tests
- ssh_config_matrix
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
54 | 59 | | |
55 | 60 | | |
56 | 61 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
58 | 63 | | |
59 | 64 | | |
60 | 65 | | |
61 | 66 | | |
62 | 67 | | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
63 | 71 | | |
64 | 72 | | |
65 | 73 | | |
| |||
0 commit comments