Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion rust/src/dns/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ mod tests {
.with_module(module()),
)
};
let cases: [(&[&str], &str); 4] = [
let cases: [(&[&str], &str); 6] = [
(
&[
"gddy",
Expand Down Expand Up @@ -124,6 +124,39 @@ mod tests {
],
"--data",
),
// A lower-case --type must require the same fields as an upper-case
// one. clap compares `required_if_eq` against the raw argument, so
// these two only hold while `--type` sets `ignore_case`.
(
&[
"gddy",
"dns",
"add",
"example.com",
"--type",
"tlsa",
"--name",
"www",
"--data",
"d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971",
],
"--usage",
),
(
&[
"gddy",
"dns",
"set",
"example.com",
"--type",
"caa",
"--name",
"@",
"--data",
"letsencrypt.org",
],
"--tag",
),
];
for (args, needle) in cases {
let output = cli().run(args.iter().copied()).await;
Expand Down
21 changes: 14 additions & 7 deletions rust/src/dns/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,13 @@ pub(super) struct RecordWriteArgs {
#[arg(value_name = "DOMAIN")]
pub(super) domain: String,

// `ignore_case` is load-bearing, not cosmetic. The `required_if_eq` predicates
// below compare against the *raw* `--type` value, not the `parse_write_type_arg`
// output — so without it `--type tlsa` (accepted, and canonicalized to `TLSA`)
// never trips the `--usage`/`--selector`/`--matching-type` requirement, and
// `--type caa` never trips `--tag`.
/// Record type (A, AAAA, ALIAS, CAA, CNAME, HTTPS, MX, SRV, SVCB, TLSA, TXT).
#[arg(long = "type", value_name = "TYPE", value_parser = parse_write_type_arg)]
#[arg(long = "type", value_name = "TYPE", value_parser = parse_write_type_arg, ignore_case = true)]
pub(super) record_type: String,

/// Record name relative to the domain (e.g. www, @ for the apex).
Expand Down Expand Up @@ -152,9 +157,10 @@ pub(super) struct RecordWriteArgs {
#[arg(long, value_name = "N", value_parser = clap::value_parser!(i64).range(0..=255))]
pub(super) flag: Option<i64>,

// A CAA record needs a tag; enforce it at parse time (before auth). The
// reverse guard — flag/tag only valid for CAA — lives in the handler
// (`validate_caa_fields`), which clap can't express.
// A CAA record needs a tag; enforce it at parse time (before auth) — which
// holds for `--type caa` as well as `--type CAA` only because `record_type`
// sets `ignore_case`. The reverse guard — flag/tag only valid for CAA —
// lives in the handler (`validate_caa_fields`), which clap can't express.
/// CAA property tag, e.g. issue/issuewild/iodef (CAA only; required for CAA).
#[arg(long, value_name = "TAG", required_if_eq("record_type", "CAA"))]
pub(super) tag: Option<String>,
Expand Down Expand Up @@ -203,9 +209,10 @@ pub(super) fn validate_caa_fields(record_type: &str, opts: &RecordOptions) -> Re

/// Validate the TLSA-specific fields against the record type. `--usage`/
/// `--selector`/`--matching-type` being present when `record_type` is TLSA is
/// already enforced by clap (`required_if_eq`); this only guards the reverse —
/// they're meaningless for other types. Pure so it's unit-testable and runs
/// before any network call.
/// enforced by clap (`required_if_eq`, which covers a lower-case `--type` only
/// because the arg sets `ignore_case`); this only guards the reverse — they're
/// meaningless for other types. Pure so it's unit-testable and runs before any
/// network call.
pub(super) fn validate_tlsa_fields(record_type: &str, opts: &RecordOptions) -> Result<(), String> {
if record_type != "TLSA"
&& (opts.usage.is_some() || opts.selector.is_some() || opts.matching_type.is_some())
Expand Down