mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Improve string validation (#1497)
This commit is contained in:
parent
ccd53d2dcc
commit
2095efe45d
|
@ -54,19 +54,32 @@ lazy_static! {
|
||||||
m.insert("administrator");
|
m.insert("administrator");
|
||||||
m
|
m
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Only lowercase+numbers, with limited chars.
|
||||||
pub static ref INAME_RE: Regex = {
|
pub static ref INAME_RE: Regex = {
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
Regex::new("^[a-z][a-z0-9-_\\.]+$").expect("Invalid Iname regex found")
|
Regex::new("^[a-z][a-z0-9-_\\.]+$").expect("Invalid Iname regex found")
|
||||||
// Only lowercase+numbers, with limited chars.
|
|
||||||
};
|
};
|
||||||
pub static ref NSUNIQUEID_RE: Regex = {
|
pub static ref NSUNIQUEID_RE: Regex = {
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
|
Regex::new("^[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}-[0-9a-fA-F]{8}$").expect("Invalid Nsunique regex found")
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Must not contain whitespace.
|
||||||
pub static ref OAUTHSCOPE_RE: Regex = {
|
pub static ref OAUTHSCOPE_RE: Regex = {
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
|
Regex::new("^[0-9a-zA-Z_]+$").expect("Invalid oauthscope regex found")
|
||||||
// Must not contain whitespace.
|
};
|
||||||
|
|
||||||
|
pub static ref SINGLELINE_RE: Regex = {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Regex::new("[\n\r\t]").expect("Invalid singleline regex found")
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static ref ESCAPES_RE: Regex = {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
Regex::new(r"\x1b\[([\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e])")
|
||||||
|
.expect("Invalid escapes regex found")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,7 +350,6 @@ pub enum PartialValue {
|
||||||
UiHint(UiHint),
|
UiHint(UiHint),
|
||||||
Passkey(Uuid),
|
Passkey(Uuid),
|
||||||
DeviceKey(Uuid),
|
DeviceKey(Uuid),
|
||||||
TrustedDeviceEnrollment(Uuid),
|
|
||||||
// The label, if any.
|
// The label, if any.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,7 +719,6 @@ impl PartialValue {
|
||||||
PartialValue::Address(a) => a.to_string(),
|
PartialValue::Address(a) => a.to_string(),
|
||||||
PartialValue::PhoneNumber(a) => a.to_string(),
|
PartialValue::PhoneNumber(a) => a.to_string(),
|
||||||
PartialValue::IntentToken(u) => u.clone(),
|
PartialValue::IntentToken(u) => u.clone(),
|
||||||
PartialValue::TrustedDeviceEnrollment(u) => u.as_hyphenated().to_string(),
|
|
||||||
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
PartialValue::UiHint(u) => (*u as u16).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -847,7 +858,6 @@ pub enum Value {
|
||||||
Passkey(Uuid, String, PasskeyV4),
|
Passkey(Uuid, String, PasskeyV4),
|
||||||
DeviceKey(Uuid, String, DeviceKeyV4),
|
DeviceKey(Uuid, String, DeviceKeyV4),
|
||||||
|
|
||||||
TrustedDeviceEnrollment(Uuid),
|
|
||||||
Session(Uuid, Session),
|
Session(Uuid, Session),
|
||||||
ApiToken(Uuid, ApiToken),
|
ApiToken(Uuid, ApiToken),
|
||||||
Oauth2Session(Uuid, Oauth2Session),
|
Oauth2Session(Uuid, Oauth2Session),
|
||||||
|
@ -1546,13 +1556,6 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_trusteddeviceenrollment(self) -> Option<(Uuid, ())> {
|
|
||||||
match self {
|
|
||||||
Value::TrustedDeviceEnrollment(u) => Some((u, ())),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_session(self) -> Option<(Uuid, Session)> {
|
pub fn to_session(self) -> Option<(Uuid, Session)> {
|
||||||
match self {
|
match self {
|
||||||
Value::Session(u, s) => Some((u, s)),
|
Value::Session(u, s) => Some((u, s)),
|
||||||
|
@ -1591,34 +1594,83 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// !!! relocate to value set !!!
|
|
||||||
pub(crate) fn validate(&self) -> bool {
|
pub(crate) fn validate(&self) -> bool {
|
||||||
// Validate that extra-data constraints on the type exist and are
|
// Validate that extra-data constraints on the type exist and are
|
||||||
// valid. IE json filter is really a filter, or cred types have supplemental
|
// valid. IE json filter is really a filter, or cred types have supplemental
|
||||||
// data.
|
// data.
|
||||||
match &self {
|
match &self {
|
||||||
Value::Iname(s) => Value::validate_iname(s),
|
// String security is required here
|
||||||
/*
|
Value::Utf8(s)
|
||||||
Value::Cred(_) => match &self.data {
|
| Value::Iutf8(s)
|
||||||
Some(v) => matches!(v.as_ref(), DataValue::Cred(_)),
|
| Value::Cred(s, _)
|
||||||
None => false,
|
| Value::PublicBinary(s, _)
|
||||||
},
|
| Value::IntentToken(s, _)
|
||||||
*/
|
| Value::Passkey(_, s, _)
|
||||||
Value::SshKey(_, key) => SshPublicKey::from_string(key).is_ok(),
|
| Value::DeviceKey(_, s, _)
|
||||||
|
| Value::TotpSecret(s, _) => {
|
||||||
|
Value::validate_str_escapes(s) && Value::validate_singleline(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Spn(a, b) => {
|
||||||
|
Value::validate_str_escapes(a)
|
||||||
|
&& Value::validate_str_escapes(b)
|
||||||
|
&& Value::validate_singleline(a)
|
||||||
|
&& Value::validate_singleline(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Iname(s) => {
|
||||||
|
Value::validate_str_escapes(s)
|
||||||
|
&& Value::validate_iname(s)
|
||||||
|
&& Value::validate_singleline(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::SshKey(s, key) => {
|
||||||
|
SshPublicKey::from_string(key).is_ok()
|
||||||
|
&& Value::validate_str_escapes(s)
|
||||||
|
&& Value::validate_singleline(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::ApiToken(_, at) => {
|
||||||
|
Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// These have stricter validators so not needed.
|
||||||
Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
|
Value::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
|
||||||
Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
Value::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
||||||
Value::EmailAddress(mail, _) => validator::validate_email(mail.as_str()),
|
Value::EmailAddress(mail, _) => validator::validate_email(mail.as_str()),
|
||||||
// PartialValue::Url validated through parsing.
|
|
||||||
Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
|
Value::OauthScope(s) => OAUTHSCOPE_RE.is_match(s),
|
||||||
Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
|
Value::OauthScopeMap(_, m) => m.iter().all(|s| OAUTHSCOPE_RE.is_match(s)),
|
||||||
_ => true,
|
|
||||||
|
Value::PhoneNumber(_, _) => true,
|
||||||
|
Value::Address(_) => true,
|
||||||
|
|
||||||
|
Value::Uuid(_)
|
||||||
|
| Value::Bool(_)
|
||||||
|
| Value::Syntax(_)
|
||||||
|
| Value::Index(_)
|
||||||
|
| Value::Refer(_)
|
||||||
|
| Value::JsonFilt(_)
|
||||||
|
| Value::SecretValue(_)
|
||||||
|
| Value::Uint32(_)
|
||||||
|
| Value::Url(_)
|
||||||
|
| Value::Cid(_)
|
||||||
|
| Value::PrivateBinary(_)
|
||||||
|
| Value::RestrictedString(_)
|
||||||
|
| Value::JwsKeyEs256(_)
|
||||||
|
| Value::Session(_, _)
|
||||||
|
| Value::Oauth2Session(_, _)
|
||||||
|
| Value::JwsKeyRs256(_)
|
||||||
|
| Value::UiHint(_) => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn validate_iname(s: &str) -> bool {
|
pub(crate) fn validate_iname(s: &str) -> bool {
|
||||||
match Uuid::parse_str(s) {
|
match Uuid::parse_str(s) {
|
||||||
// It is a uuid, disallow.
|
// It is a uuid, disallow.
|
||||||
Ok(_) => false,
|
Ok(_) => {
|
||||||
|
warn!("iname values may not contain uuids");
|
||||||
|
false
|
||||||
|
}
|
||||||
// Not a uuid, check it against the re.
|
// Not a uuid, check it against the re.
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if !INAME_RE.is_match(s) {
|
if !INAME_RE.is_match(s) {
|
||||||
|
@ -1633,6 +1685,31 @@ impl Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_singleline(s: &str) -> bool {
|
||||||
|
if !SINGLELINE_RE.is_match(s) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"value contains invalid whitespace chars forbidden by \"{}\"",
|
||||||
|
*SINGLELINE_RE
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn validate_str_escapes(s: &str) -> bool {
|
||||||
|
// Look for and prevent certain types of string escapes and injections.
|
||||||
|
if !ESCAPES_RE.is_match(s) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"value contains invalid escape chars forbidden by \"{}\"",
|
||||||
|
*ESCAPES_RE
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1867,4 +1944,21 @@ mod tests {
|
||||||
assert!(val2.is_some());
|
assert!(val2.is_some());
|
||||||
assert!(val3.is_some());
|
assert!(val3.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_singleline() {
|
||||||
|
assert!(Value::validate_singleline("no new lines"));
|
||||||
|
|
||||||
|
assert!(!Value::validate_singleline("contains a \n new line"));
|
||||||
|
assert!(!Value::validate_singleline("contains a \r return feed"));
|
||||||
|
assert!(!Value::validate_singleline("contains a \t tab char"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_str_escapes() {
|
||||||
|
assert!(Value::validate_str_escapes("safe str"));
|
||||||
|
assert!(Value::validate_str_escapes("🙃 emoji are 👍"));
|
||||||
|
|
||||||
|
assert!(!Value::validate_str_escapes("naughty \x1b[31mred"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,9 @@ impl ValueSetT for ValueSetPublicBinary {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map
|
||||||
|
.iter()
|
||||||
|
.all(|(s, _)| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -119,7 +119,9 @@ impl ValueSetT for ValueSetCredential {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map
|
||||||
|
.iter()
|
||||||
|
.all(|(s, _)| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
@ -333,7 +335,9 @@ impl ValueSetT for ValueSetIntentToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map
|
||||||
|
.iter()
|
||||||
|
.all(|(s, _)| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
@ -540,7 +544,9 @@ impl ValueSetT for ValueSetPasskey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map
|
||||||
|
.iter()
|
||||||
|
.all(|(_, (s, _))| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
@ -724,7 +730,9 @@ impl ValueSetT for ValueSetDeviceKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map
|
||||||
|
.iter()
|
||||||
|
.all(|(_, (s, _))| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -102,7 +102,11 @@ impl ValueSetT for ValueSetIname {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
self.set.iter().all(|s| Value::validate_iname(s.as_str()))
|
self.set.iter().all(|s| {
|
||||||
|
Value::validate_str_escapes(s)
|
||||||
|
&& Value::validate_singleline(s)
|
||||||
|
&& Value::validate_iname(s.as_str())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -103,7 +103,9 @@ impl ValueSetT for ValueSetIutf8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.set
|
||||||
|
.iter()
|
||||||
|
.all(|s| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -583,7 +583,6 @@ pub fn from_result_value_iter(
|
||||||
| Value::Passkey(_, _, _)
|
| Value::Passkey(_, _, _)
|
||||||
| Value::DeviceKey(_, _, _)
|
| Value::DeviceKey(_, _, _)
|
||||||
| Value::TotpSecret(_, _)
|
| Value::TotpSecret(_, _)
|
||||||
| Value::TrustedDeviceEnrollment(_)
|
|
||||||
| Value::Session(_, _)
|
| Value::Session(_, _)
|
||||||
| Value::ApiToken(_, _)
|
| Value::ApiToken(_, _)
|
||||||
| Value::Oauth2Session(_, _)
|
| Value::Oauth2Session(_, _)
|
||||||
|
@ -646,7 +645,7 @@ pub fn from_value_iter(mut iter: impl Iterator<Item = Value>) -> Result<ValueSet
|
||||||
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
Value::Oauth2Session(u, m) => ValueSetOauth2Session::new(u, m),
|
||||||
Value::UiHint(u) => ValueSetUiHint::new(u),
|
Value::UiHint(u) => ValueSetUiHint::new(u),
|
||||||
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
Value::TotpSecret(l, t) => ValueSetTotpSecret::new(l, t),
|
||||||
Value::PhoneNumber(_, _) | Value::TrustedDeviceEnrollment(_) => {
|
Value::PhoneNumber(_, _) => {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
return Err(OperationError::InvalidValueState);
|
return Err(OperationError::InvalidValueState);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1088,7 +1088,9 @@ impl ValueSetT for ValueSetApiToken {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map.iter().all(|(_, at)| {
|
||||||
|
Value::validate_str_escapes(&at.label) && Value::validate_singleline(&at.label)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -96,7 +96,12 @@ impl ValueSetT for ValueSetSpn {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.set.iter().all(|(a, b)| {
|
||||||
|
Value::validate_str_escapes(a)
|
||||||
|
&& Value::validate_str_escapes(b)
|
||||||
|
&& Value::validate_singleline(a)
|
||||||
|
&& Value::validate_singleline(b)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -7,6 +7,8 @@ use crate::repl::proto::ReplAttrV1;
|
||||||
use crate::schema::SchemaAttribute;
|
use crate::schema::SchemaAttribute;
|
||||||
use crate::valueset::{DbValueSetV2, ValueSet};
|
use crate::valueset::{DbValueSetV2, ValueSet};
|
||||||
|
|
||||||
|
use sshkeys::PublicKey as SshPublicKey;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ValueSetSshKey {
|
pub struct ValueSetSshKey {
|
||||||
map: BTreeMap<String, String>,
|
map: BTreeMap<String, String>,
|
||||||
|
@ -102,7 +104,11 @@ impl ValueSetT for ValueSetSshKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.map.iter().all(|(s, key)| {
|
||||||
|
SshPublicKey::from_string(key).is_ok()
|
||||||
|
&& Value::validate_str_escapes(s)
|
||||||
|
&& Value::validate_singleline(s)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
|
@ -88,7 +88,9 @@ impl ValueSetT for ValueSetUtf8 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
fn validate(&self, _schema_attr: &SchemaAttribute) -> bool {
|
||||||
true
|
self.set
|
||||||
|
.iter()
|
||||||
|
.all(|s| Value::validate_str_escapes(s) && Value::validate_singleline(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
|
||||||
|
|
Loading…
Reference in a new issue