Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Provide path to avoid UTF-8 panics #2677

Merged
merged 1 commit into from
Aug 18, 2021
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ Added `unicode_help`, `env` features.
* **ErrorKind**
* `ErrorKind::MissingArgumentOrSubcommand` => `ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand`
* **Changed**
* `AppSettings::StrictUtf8` is now default and it and `AppSettings::AllowInvalidUtf8` are replaced by
* `AppSettings::AllowInvalidUtf8ForExternalSubcommands`
* This only applies to the subcommand args. Before we paniced if the
subcommand itself was invalid but now we will report an error up to the
user.
* `ArgSettings::AllowInvalidUtf8`
* Allowing empty values is the default again with `ArgSettings::AllowEmptyValues` changing to
`ArgSettings::ForbidEmptyValues`
* `AppSettings::GlobalVersion` renamed to `AppSettings::PropagateVersion` and it is not applied
Expand Down
22 changes: 21 additions & 1 deletion clap_derive/src/derives/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,22 @@ pub fn gen_augment(
ParserKind::TryFromOsStr => quote_spanned! { func.span()=>
.validator_os(|s| #func(s).map(|_: #convert_type| ()))
},
_ => quote!(),
ParserKind::FromStr
| ParserKind::FromOsStr
| ParserKind::FromFlag
| ParserKind::FromOccurrences => quote!(),
pksunkara marked this conversation as resolved.
Show resolved Hide resolved
};
let allow_invalid_utf8 = match *parser.kind {
_ if attrs.is_enum() => quote!(),
ParserKind::FromOsStr | ParserKind::TryFromOsStr => {
quote_spanned! { func.span()=>
.allow_invalid_utf8(true)
}
}
ParserKind::FromStr
| ParserKind::TryFromStr
| ParserKind::FromFlag
| ParserKind::FromOccurrences => quote!(),
pksunkara marked this conversation as resolved.
Show resolved Hide resolved
};

let value_name = attrs.value_name();
Expand All @@ -250,6 +265,7 @@ pub fn gen_augment(
.value_name(#value_name)
#possible_values
#validator
#allow_invalid_utf8
}
}

Expand All @@ -260,6 +276,7 @@ pub fn gen_augment(
.max_values(1)
.multiple_values(false)
#validator
#allow_invalid_utf8
},

Ty::OptionVec => quote_spanned! { ty.span()=>
Expand All @@ -268,6 +285,7 @@ pub fn gen_augment(
.multiple_values(true)
.min_values(0)
#validator
#allow_invalid_utf8
},

Ty::Vec => {
Expand All @@ -285,6 +303,7 @@ pub fn gen_augment(
.multiple_values(true)
#possible_values
#validator
#allow_invalid_utf8
}
}

Expand All @@ -311,6 +330,7 @@ pub fn gen_augment(
.required(#required)
#possible_values
#validator
#allow_invalid_utf8
}
}
};
Expand Down
32 changes: 29 additions & 3 deletions clap_derive/src/derives/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,34 @@ fn gen_augment(

match &*kind {
Kind::ExternalSubcommand => {
quote_spanned! { kind.span()=>
let app = app.setting(clap::AppSettings::AllowExternalSubcommands);
let ty = match variant.fields {
Unnamed(ref fields) if fields.unnamed.len() == 1 => &fields.unnamed[0].ty,

_ => abort!(
variant,
"The enum variant marked with `external_subcommand` must be \
a single-typed tuple, and the type must be either `Vec<String>` \
or `Vec<OsString>`."
),
};
match subty_if_name(ty, "Vec") {
Some(subty) => {
if is_simple_ty(subty, "OsString") {
quote_spanned! { kind.span()=>
let app = app.setting(clap::AppSettings::AllowExternalSubcommands).setting(clap::AppSettings::AllowInvalidUtf8ForExternalSubcommands);
}
} else {
pksunkara marked this conversation as resolved.
Show resolved Hide resolved
quote_spanned! { kind.span()=>
let app = app.setting(clap::AppSettings::AllowExternalSubcommands);
}
}
}

None => abort!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add ui tests for all the aborts you added?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just duplicated existing aborts, just moving them up earlier in the process. I at least confirmed the subcommand abort is already covered

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both the aborts are for different scenarios, so, I would still expect different tests here since this scenario is not covered.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear which aborts we are talking about, I'm assuming you are referring to

                        _ => abort!(
                            variant,
                            "The enum variant marked with `external_subcommand` must be \
                             a single-typed tuple, and the type must be either `Vec<String>` \
                             or `Vec<OsString>`."
                        ),

and

                        None => abort!(
                            ty.span(),
                            "The type must be either `Vec<String>` or `Vec<OsString>` \
                             to be used with `external_subcommand`."
                        ),

To boil these two aborts down, they are:

  • Invalid enum variant for external subcommands
  • Invalid type in Vec inside external subcommands

These checks were added to gen_augment but already exist in gen_from_arg_matches and are already covered by clap_derive/tests/ui/external_subcommand_wrong_type.rs

use clap::Clap;
use std::ffi::CString;

#[derive(Clap, Debug)]
enum Opt {
    #[clap(external_subcommand)]
    Other(Vec<CString>),
}

#[derive(Clap, Debug)]
enum Opt2 {
    #[clap(external_subcommand)]
    Other(String),
}

#[derive(Clap, Debug)]
enum Opt3 {
    #[clap(external_subcommand)]
    Other { a: String },
}

fn main() {
    let _ = Opt::parse();
    let _ = Opt2::parse();
    let _ = Opt3::parse();
}

Copy link
Member

@pksunkara pksunkara Aug 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so you said you duplicated 2 aborts in this PR from old code.

The first one is this which is duplicated from this. Since gen_from_arg_matches is called first before gen_augment is called, we don't need the new abort. We can guarantee that new abort never triggers which means we need an INTERNAL_ERR panic here.

The second one is this which is duplicated from this. But we are not handling Vec<String> here which means the phrasing here should be different. And we need to make sure both the phrasings are tested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment with edits to make it more clearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having one function abort on behalf of both functions creates a behavior dependency between them that is non-obvious and can easily break as code is refactored in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about asking to move this to be above both of these functions. But this is too much time/discussion for a simple thing. Forget about the first abort. Let's just resolve the second abort as described above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead though the other one has precedence so the change does not show up in error messages.

ty.span(),
"The type must be `Vec<_>` \
to be used with `external_subcommand`."
),
}
}

Expand Down Expand Up @@ -348,7 +374,7 @@ fn gen_from_arg_matches(

_ => abort!(
variant,
"The enum variant marked with `external_attribute` must be \
"The enum variant marked with `external_subcommand` must be \
a single-typed tuple, and the type must be either `Vec<String>` \
or `Vec<OsString>`."
),
Expand Down
2 changes: 1 addition & 1 deletion clap_derive/tests/ui/external_subcommand_wrong_type.stderr
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ error: The type must be either `Vec<String>` or `Vec<OsString>` to be used with
13 | Other(String),
| ^^^^^^

error: The enum variant marked with `external_attribute` must be a single-typed tuple, and the type must be either `Vec<String>` or `Vec<OsString>`.
error: The enum variant marked with `external_subcommand` must be a single-typed tuple, and the type must be either `Vec<String>` or `Vec<OsString>`.
--> $DIR/external_subcommand_wrong_type.rs:18:5
|
18 | / #[clap(external_subcommand)]
Expand Down
226 changes: 226 additions & 0 deletions clap_derive/tests/utf8.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
#![cfg(not(windows))]

use clap::{Clap, ErrorKind};
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;

#[derive(Clap, Debug, PartialEq, Eq)]
struct Positional {
arg: String,
}

#[derive(Clap, Debug, PartialEq, Eq)]
struct Named {
#[clap(short, long)]
arg: String,
}

#[test]
fn invalid_utf8_strict_positional() {
let m = Positional::try_parse_from(vec![OsString::from(""), OsString::from_vec(vec![0xe9])]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn invalid_utf8_strict_option_short_space() {
let m = Named::try_parse_from(vec![
OsString::from(""),
OsString::from("-a"),
OsString::from_vec(vec![0xe9]),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn invalid_utf8_strict_option_short_equals() {
let m = Named::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x61, 0x3d, 0xe9]),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn invalid_utf8_strict_option_short_no_space() {
let m = Named::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x61, 0xe9]),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn invalid_utf8_strict_option_long_space() {
let m = Named::try_parse_from(vec![
OsString::from(""),
OsString::from("--arg"),
OsString::from_vec(vec![0xe9]),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn invalid_utf8_strict_option_long_equals() {
let m = Named::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x2d, 0x61, 0x72, 0x67, 0x3d, 0xe9]),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[derive(Clap, Debug, PartialEq, Eq)]
struct PositionalOs {
#[clap(parse(from_os_str))]
arg: OsString,
}

#[derive(Clap, Debug, PartialEq, Eq)]
struct NamedOs {
#[clap(short, long, parse(from_os_str))]
arg: OsString,
}

#[test]
fn invalid_utf8_positional() {
let r = PositionalOs::try_parse_from(vec![OsString::from(""), OsString::from_vec(vec![0xe9])]);
assert_eq!(
r.unwrap(),
PositionalOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[test]
fn invalid_utf8_option_short_space() {
let r = NamedOs::try_parse_from(vec![
OsString::from(""),
OsString::from("-a"),
OsString::from_vec(vec![0xe9]),
]);
assert_eq!(
r.unwrap(),
NamedOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[test]
fn invalid_utf8_option_short_equals() {
let r = NamedOs::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x61, 0x3d, 0xe9]),
]);
assert_eq!(
r.unwrap(),
NamedOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[test]
fn invalid_utf8_option_short_no_space() {
let r = NamedOs::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x61, 0xe9]),
]);
assert_eq!(
r.unwrap(),
NamedOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[test]
fn invalid_utf8_option_long_space() {
let r = NamedOs::try_parse_from(vec![
OsString::from(""),
OsString::from("--arg"),
OsString::from_vec(vec![0xe9]),
]);
assert_eq!(
r.unwrap(),
NamedOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[test]
fn invalid_utf8_option_long_equals() {
let r = NamedOs::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0x2d, 0x2d, 0x61, 0x72, 0x67, 0x3d, 0xe9]),
]);
assert_eq!(
r.unwrap(),
NamedOs {
arg: OsString::from_vec(vec![0xe9])
}
);
}

#[derive(Debug, PartialEq, Clap)]
enum External {
#[clap(external_subcommand)]
Other(Vec<String>),
}

#[test]
fn refuse_invalid_utf8_subcommand_with_allow_external_subcommands() {
let m = External::try_parse_from(vec![
OsString::from(""),
OsString::from_vec(vec![0xe9]),
OsString::from("normal"),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[test]
fn refuse_invalid_utf8_subcommand_args_with_allow_external_subcommands() {
let m = External::try_parse_from(vec![
OsString::from(""),
OsString::from("subcommand"),
OsString::from("normal"),
OsString::from_vec(vec![0xe9]),
OsString::from("--another_normal"),
]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::InvalidUtf8);
}

#[derive(Debug, PartialEq, Clap)]
enum ExternalOs {
#[clap(external_subcommand)]
Other(Vec<OsString>),
}

#[test]
fn allow_invalid_utf8_subcommand_args_with_allow_external_subcommands() {
let m = ExternalOs::try_parse_from(vec![
OsString::from(""),
OsString::from("subcommand"),
OsString::from("normal"),
OsString::from_vec(vec![0xe9]),
OsString::from("--another_normal"),
]);
assert_eq!(
m.unwrap(),
ExternalOs::Other(vec![
OsString::from("subcommand"),
OsString::from("normal"),
OsString::from_vec(vec![0xe9]),
OsString::from("--another_normal"),
])
);
}
25 changes: 25 additions & 0 deletions src/build/app/debug_asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ pub(crate) fn assert_app(app: &App) {
detect_duplicate_flags(&short_flags, "short");

app._panic_on_missing_help(app.g_settings.is_set(AppSettings::HelpRequired));
assert_app_flags(app);
}

fn detect_duplicate_flags(flags: &[Flag], short_or_long: &str) {
Expand Down Expand Up @@ -301,3 +302,27 @@ fn find_duplicates<T: PartialEq>(slice: &[T]) -> impl Iterator<Item = (&T, &T)>
}
})
}

fn assert_app_flags(app: &App) {
use AppSettings::*;

macro_rules! checker {
($a:ident requires $($b:ident)|+) => {
if app.is_set($a) {
let mut s = String::new();

$(
if !app.is_set($b) {
s.push_str(&format!("\nAppSettings::{} is required when AppSettings::{} is set.\n", std::stringify!($b), std::stringify!($a)));
}
)+

if !s.is_empty() {
panic!("{}", s)
}
}
}
}

checker!(AllowInvalidUtf8ForExternalSubcommands requires AllowExternalSubcommands);
}
Loading