fix(toolchain)!: restrict named toolchain characters - #4932
Conversation
|
Note to myself that this will probably need some corrections once #4930 is merged |
1318f92 to
9118fcc
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@cachebag Thanks for this PR! I think given your comment in #4932 (comment) I'll probably review #4930 first and come back to this one later, many thanks for your understanding 🙏 |
86f5e64 to
b8c8bb9
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
913f977 to
a8ed506
Compare
| Ok(custom) => Ok(Self::Custom(custom)), | ||
| Err(_) => Err(InvalidName::ToolchainName(candidate.into())), | ||
| } | ||
| let candidate = validate_named_toolchain(candidate)?; |
There was a problem hiding this comment.
Nit: Given that the previous change #4930 has focused on rewriting the existing validation code from functional to imperative, suggest keeping the imperative style here to minimize the diff. Same with the other functions that you may or may not have changed in this patch.
| } | ||
| } | ||
|
|
||
| fn validate_named_toolchain(candidate: &str) -> Result<&str, InvalidName> { |
There was a problem hiding this comment.
Question: what is the rationale for having two distinct functions validate() and validate_named_toolchain() in this module?
There was a problem hiding this comment.
As touched on in #4932 (comment) - validate() is a common baseline used by both named and path-capable inputs. The latter is stricter for the purpose of the introduced allowlist
There was a problem hiding this comment.
Sorry for not having expressed my intentions super clearly. My question is rather: after your changes, do the validate() function still have any reasons to exist? Are there any cases where we want the normalization without the legality check/validation?
If not or if it becomes single-used, then maybe it's better just to keep the actual "validation" function to avoid confusions.
There was a problem hiding this comment.
Right- so validate() is still needed because ResolvableLocalToolchainName::validate uses it to handle both named toolchains and paths, so it needs the basic normalization without the character allowlist.
Lines 293 to 305 in 3432d80
More concretely, it's used in three places: ResolvableLocalToolchainName::validate, MaybeResolvableToolchainName::validate and validate_named_toolchain itself.
validate_named_toolchain() builds on top of it for the stricter named-only callers.
I've applied your suggestion for CustomToolchainName. It now uses validate_named_toolchain() and is_legal_named_toolchain has been inlined.
Is there something I am misunderstanding? Are you asking if we should inline validate()'s logic? If so, that repeats code more than we would be by just keeping these two functions separate.
Though maybe I am just misunderstanding your question completely, and if so then pardon me.
| } | ||
| } | ||
|
|
||
| fn is_legal_named_toolchain(candidate: &str) -> bool { |
There was a problem hiding this comment.
Question: How is it/why should it be different from validate()/validate_named_toolchain()?
There was a problem hiding this comment.
validate() only does shared normalization/basic checks: reject +, trim trailing /, reject empty. It still allows paths and old broad custom names.
validate_named_toolchain() builds on that and rejects illegal named-toolchain characters, returning a ToolchainName error.
It feels to me like it is the reusable predicate for just the allowlist. I wrote it because CustomToolchainName::validate() needs the same allowlist but must return CustomName, not ToolchainName.
That being said the names are not great. We could probably have them be normalize_toolchain_name(), is_valid_toolchain_name and validate_toolchain_name()?
There was a problem hiding this comment.
Looking at your proposed name it looks like is_valid_toolchain_name(name) can be inlined as validate_toolchain_name(name).is_ok(). Of course this is not the case currently but I can't help thinking about moving towards that direction.
Looking at the docstring of validate():
/// Common validate rules for all sorts of toolchain names
It looks like we should just do all possible verification passes in one place? Would that be possible? Something like the following, for example:
impl CustomToolchainName {
fn validate(candidate: &str) -> Result<Self, InvalidName> {
- let candidate = validate(candidate)?;
+ // At this point there is already no need to check for `/` and `\`.
+ let candidate = validate_toolchain_name(candidate)?;
if candidate.parse::<PartialToolchainDesc>().is_ok()
|| candidate == "none"
- || candidate.contains('/')
- || candidate.contains('\\')
{
return Err(InvalidName::CustomName(candidate.into()))
}
Ok(Self(candidate.into()))
}
}What I am trying to say is that in that case is_valid_toolchain_name(name) is not needed anymore.
| } | ||
| } | ||
|
|
||
| fn is_legal_named_toolchain(candidate: &str) -> bool { |
There was a problem hiding this comment.
Nit: If we decide to keep it, it looks like it should be called is_legal_toolchain_name().
There was a problem hiding this comment.
See above responses.
I do like is_legal_toolchain_name() more than is_valid_...()
There was a problem hiding this comment.
Why? We have a bunch of validate_* functions to I like that is_valid_ references that more clearly than introducing a seemingly separate concept of "legality".
There was a problem hiding this comment.
"legal" seemed to reflect the new allowlist/policy. i'm not hard pressed on the naming convention, is_valid_ is better for your reasons, that makes sense to me from a code standpoint
6d57895 to
c1a8e40
Compare
b1e3812 to
39fbbcc
Compare
|
@cachebag Sorry for the delay in reviewing! I have to see if the current code can be further streamlined and what we should do about #4059 (comment). Please stay tuned... |
8e4516c to
39f608c
Compare
d14a77d to
4259f6e
Compare
This comment has been minimized.
This comment has been minimized.
0bc07d2 to
2eecf4a
Compare
Validate named toolchains with the UTS rust-lang#39 general security profile via `unicode_security::GeneralSecurityProfile::identifier_allowed()` instead of an ASCII allowlist, following the approach sketched in rust-lang#4059. Letters and digits in any script are now legal, so `合法的` works as a custom toolchain name. ASCII letters, digits, `.`, `_` and `-` remain allowed, so every official toolchain name still parses. Whitespace, most punctuation, emoji, and invisible or direction-altering characters such as U+202E RIGHT-TO-LEFT OVERRIDE are still rejected. Two characters the profile permits are excluded anyway: `:`, because a named toolchain becomes a directory under `.rustup/toolchains` and `name:stream` denotes an NTFS alternate data stream on Windows, and `'`, which needs quoting in too many shells to be worth allowing. `.` and `..` are rejected explicitly, since the profile permits both. Confusables remain unresolved: `μ` is accepted while `µ` is not, and precomposed and decomposed `é` are distinct names. That matches the existing status quo.
`try_from_str!()` fed `TryFrom<String>`, `TryFrom<&str>` and `FromStr` off a single inherent `validate()`, which is why each name type had one. With the macro gone every such method had exactly one caller, its own `from_str()`, so move the bodies there and drop the methods. Rename the free `validate()` to `normalize_name()`, which is precisely what it does: strips a `+` prefix, trims trailing slashes, and rejects empty names.
Partially addresses #4059 by restricting what characters a named toolchain may contain.
Named toolchains are now validated against the UTS #39 general security profile.
In essence;
unicode_security::GeneralSecurityProfile::identifier_allowed()vs this hand-rolled pattern we had before. Letters and digits in any script stay legal, so合法的still works, as do.,_and-. Whitespace, most punctuation, emoji, and invisible or direction-altering characters are rejected. I also exclude:(NTFS alternate data streams) and'(shell quoting), plus.and.., which the profile allows.This is breaking for unusual names like
foo#baror names with spaces. However it doesn't touch therust-toolchain.tomldiagnostics. Confusables are also still unresolved (i.e.μvsµ, precomposed vs decomposedé).