diff --git a/rust/lancedb/src/remote/db.rs b/rust/lancedb/src/remote/db.rs index dd029325f..308c3c198 100644 --- a/rust/lancedb/src/remote/db.rs +++ b/rust/lancedb/src/remote/db.rs @@ -34,7 +34,7 @@ use crate::remote::job::{RemoteJob, job_state_to_client}; use crate::remote::util::stream_as_body; use crate::secrets::SecretInfo; use crate::table::BaseTable; -use crate::utils::{validate_namespace_name, validate_table_name}; +use crate::utils::{reject_relative_segment, validate_namespace_name, validate_table_name}; use super::client::{ ClientConfig, HeaderProvider, HttpSend, RequestResultExt, RestfulLanceDbClient, Sender, @@ -366,78 +366,6 @@ impl RemoteDatabase { /// /// The value is a request field and never a path segment or query /// parameter, which keeps it out of access logs and proxy traces. - /// The path segment addressing one Secret. - /// - /// Written into the path as-is, the way a table identifier is. The charset - /// a Secret name and a namespace segment admit excludes everything that - /// would need escaping, and a `/v1/secret/prod$openai/drop` that reads like - /// the table and Function routes beside it is worth more than encoding - /// against a charset that does not exist yet. - /// The path segment addressing one Secret. - /// - /// The shape of each component is settled by then -- [`Connection`] checks - /// that before anything reaches a transport. What is left is the one rule - /// that cannot be checked there, because it depends on configuration this - /// layer owns: a component may not contain the delimiter it is about to be - /// joined with. - /// - /// [`Connection`]: crate::connection::Connection - fn secret_id(&self, name: &str, namespace_path: &[String]) -> Result { - self.reject_delimiter_in_components( - namespace_path.iter().map(String::as_str).chain([name]), - )?; - Ok(build_secret_identifier( - name, - namespace_path, - &self.client.id_delimiter, - )) - } - - fn secret_namespace_id(&self, namespace_path: &[String]) -> Result { - self.reject_delimiter_in_components(namespace_path.iter().map(String::as_str))?; - Ok(build_secret_namespace_identifier( - namespace_path, - &self.client.id_delimiter, - )) - } - - /// Refuse a component holding the delimiter that is about to separate it - /// from the others. - /// - /// The default `$` is outside the character set a Secret admits, so this - /// never fires for it -- [`Connection`] has already refused any component - /// that could contain one. It exists for a *configured* delimiter, which - /// may be any string at all and is most usefully one that reads well: - /// `.`, `-` and `_` are the likely choices and all three are inside that - /// set. - /// - /// Under `id_delimiter="."` the component `prod.vision` and the pair - /// `prod`, `vision` join to one string, and the caller who wrote the first - /// would address the Secret belonging to the second. Refusing is the only - /// answer that keeps the identity the caller wrote: there is no escaping - /// of the joined form that recovers a boundary the delimiter has already - /// erased. - /// - /// [`Connection`]: crate::connection::Connection - fn reject_delimiter_in_components<'a>( - &self, - components: impl Iterator, - ) -> Result<()> { - let delimiter = &self.client.id_delimiter; - for component in components { - if component.contains(delimiter.as_str()) { - return Err(Error::InvalidInput { - message: format!( - "Secret identifier component '{component}' contains the configured \ - identifier delimiter '{delimiter}', so the namespace path and the name \ - it joins could not be told apart" - ), - }); - } - } - Ok(()) - } - async fn post_secret_write(&self, route: &str, body: &T) -> Result<()> { let req = self.client.post(route).json(body); // This call is what says the body is a credential. Nothing downstream @@ -585,6 +513,7 @@ impl From<&CreateTableMode> for &'static str { } } +/// The path segment addressing one table: its namespace path and name. fn build_table_identifier(name: &str, namespace: &[String], delimiter: &str) -> Result { for segment in namespace { validate_namespace_name(segment)?; @@ -592,43 +521,20 @@ fn build_table_identifier(name: &str, namespace: &[String], delimiter: &str) -> } validate_table_name(name)?; reject_delimiter(name, delimiter, "table name")?; - if namespace.is_empty() { - return Ok(name.to_string()); - } - let mut parts = namespace.to_vec(); - parts.push(name.to_string()); - Ok(parts.join(delimiter)) + Ok(join_identifier( + namespace.iter().map(String::as_str).chain([name]), + delimiter, + )) } /// The path segment addressing one Function. /// -/// A Function has no namespace yet, so there is no join to reverse and nothing -/// a delimiter could split wrongly. What remains is the part encoding cannot -/// fix: `..` is unreserved, so it survives percent-encoding untouched and is -/// then resolved as a relative path segment -- `create_function` for a Function -/// so named would post its registration body to `/v1/create`. +/// A Function has no namespace, so there is one component and no join to +/// reverse. What encoding cannot fix is `..`: unreserved, so it survives +/// untouched and is then resolved away. fn build_function_identifier(name: &str) -> Result { - reject_relative_segment_component("Function name", name)?; - Ok(urlencoding::encode(name).into_owned()) -} - -/// The relative-segment rule, for identifiers that have no other validator to -/// hang it on. -/// -/// Table and namespace names reach this through [`validate_table_name`] and -/// [`validate_namespace_name`], which own the rest of their grammar. A Function -/// name has no client-side grammar, so only this part applies. -fn reject_relative_segment_component(what: &str, value: &str) -> Result<()> { - let decoded = value.replace("%2e", ".").replace("%2E", "."); - if decoded == "." || decoded == ".." { - return Err(Error::InvalidInput { - message: format!( - "invalid {what} '{value}': '.' and '..' are read as relative path segments and \ - cannot address an object" - ), - }); - } - Ok(()) + reject_relative_segment("Function name", name)?; + Ok(join_identifier([name].into_iter(), "")) } /// Refuse a component holding the delimiter that is about to separate it from @@ -657,68 +563,61 @@ fn reject_delimiter(component: &str, delimiter: &str, what: &str) -> Result<()> Ok(()) } -/// A Secret's path identifier: its namespace path and name, each percent-encoded, -/// joined by the delimiter. +/// The path segment addressing one Secret: its namespace path and name. /// -/// The grammar is the namespace spec's, shared with tables and Functions. What -/// differs is that the components are encoded before they are joined, rather -/// than the joined string being encoded or nothing being encoded at all. -/// -/// Encoding per component is what keeps a component from becoming two. The -/// delimiter is joined raw, so it stays a delimiter; anything inside a -/// component that would otherwise end the path segment does not survive to do -/// it. `..` and `/` in a name are the cases that matter: unencoded they leave -/// `/v1/secret/` entirely and carry the request body -- a credential, on the -/// write verbs -- to whatever route the normalized path lands on. -/// -/// This is not name validation, which belongs to the service. Every character -/// a Secret name and a namespace segment may hold is already unreserved, so an -/// identifier the service would accept passes through byte-identical and reads -/// like the table and Function identifiers beside it. One the service would -/// refuse is merely made to arrive, so that it can be refused. -fn build_secret_identifier(name: &str, namespace: &[String], delimiter: &str) -> String { - namespace - .iter() - .map(String::as_str) - .chain(std::iter::once(name)) - .map(encode_identifier_component) - .collect::>() - .join(delimiter) -} - -/// One component of an identifier, escaped for a path segment. -/// -/// A second line rather than the first. [`Connection`] refuses any component -/// that is not alphanumerics, `_`, `-` or `.`, and every one of those is -/// unreserved -- so a component that reaches here encodes to itself and the -/// route reads exactly as the table and Function routes beside it do. This -/// stands between a future caller that reaches the transport another way and -/// a URL whose structure its input chose. -/// -/// It is not sufficient on its own, which is why it is not the only check: -/// `.` and `..` are unreserved too, and are resolved as relative segments -/// after decoding, so no escaping of them survives. Those are refused -/// outright. +/// Component shape is settled before this, by [`Connection`], rather than here +/// as it is for a table -- a Secret is reached only through that one entry +/// point. /// /// [`Connection`]: crate::connection::Connection -fn encode_identifier_component(component: &str) -> String { - urlencoding::encode(component).into_owned() +fn build_secret_identifier(name: &str, namespace: &[String], delimiter: &str) -> Result { + for segment in namespace { + reject_delimiter(segment, delimiter, "Secret namespace path segment")?; + } + reject_delimiter(name, delimiter, "Secret name")?; + Ok(join_identifier( + namespace.iter().map(String::as_str).chain([name]), + delimiter, + )) } -/// The namespace a Secret listing is scoped to, encoded the same way. +/// Join validated components into the `{id}` a route addresses: each +/// percent-encoded, then joined by the delimiter. /// -/// The root is the bare delimiter, as it is for every other object's listing. -fn build_secret_namespace_identifier(namespace: &[String], delimiter: &str) -> String { - if namespace.is_empty() { - return delimiter.to_string(); - } - namespace - .iter() - .map(|segment| encode_identifier_component(segment)) +/// Encoded per component rather than over the joined string, so the delimiter +/// stays a delimiter and nothing inside a component can end the path segment. +/// +/// It is a second line, not the first. Every character the name charset admits +/// is unreserved, so a component that reached here encodes to itself and the +/// route reads exactly as the caller wrote it; what the encoding covers is a +/// component that arrives some other way, so that its content cannot choose the +/// URL's shape. It does not cover `.` and `..`, which are unreserved too and +/// are resolved after decoding -- those are refused outright, by the validator +/// each object has. +fn join_identifier<'a>(components: impl Iterator, delimiter: &str) -> String { + components + .map(|component| urlencoding::encode(component).into_owned()) .collect::>() .join(delimiter) } +/// The namespace a Secret listing is scoped to. +/// +/// The root is the bare delimiter, as it is for every other object's listing. +fn build_secret_namespace_identifier(namespace: &[String], delimiter: &str) -> Result { + for segment in namespace { + reject_delimiter(segment, delimiter, "Secret namespace path segment")?; + } + if namespace.is_empty() { + return Ok(delimiter.to_string()); + } + Ok(join_identifier( + namespace.iter().map(String::as_str), + delimiter, + )) +} + +/// The path segment addressing one namespace. fn build_namespace_identifier(namespace: &[String], delimiter: &str) -> Result { for segment in namespace { validate_namespace_name(segment)?; @@ -728,7 +627,10 @@ fn build_namespace_identifier(namespace: &[String], delimiter: &str) -> Result Database for RemoteDatabase { value: &str, namespace_path: &[String], ) -> Result<()> { - let secret_id = self.secret_id(name, namespace_path)?; + let secret_id = build_secret_identifier(name, namespace_path, &self.client.id_delimiter)?; self.post_secret_write( &format!("/v1/secret/{secret_id}/create"), &RemoteCreateSecretRequest { value }, @@ -1094,7 +996,7 @@ impl Database for RemoteDatabase { } async fn alter_secret(&self, name: &str, value: &str, namespace_path: &[String]) -> Result<()> { - let secret_id = self.secret_id(name, namespace_path)?; + let secret_id = build_secret_identifier(name, namespace_path, &self.client.id_delimiter)?; self.post_secret_write( &format!("/v1/secret/{secret_id}/alter"), &RemoteAlterSecretRequest { value }, @@ -1103,7 +1005,8 @@ impl Database for RemoteDatabase { } async fn list_secrets(&self, namespace_path: &[String]) -> Result> { - let namespace_id = self.secret_namespace_id(namespace_path)?; + let namespace_id = + build_secret_namespace_identifier(namespace_path, &self.client.id_delimiter)?; let path = format!("/v1/namespace/{namespace_id}/secret/list"); let mut names = Vec::new(); let mut page_token: Option = None; @@ -1136,7 +1039,7 @@ impl Database for RemoteDatabase { } async fn drop_secret(&self, name: &str, namespace_path: &[String]) -> Result<()> { - let secret_id = self.secret_id(name, namespace_path)?; + let secret_id = build_secret_identifier(name, namespace_path, &self.client.id_delimiter)?; let req = self.client.post(&format!("/v1/secret/{secret_id}/drop")); let (request_id, response) = self.client.send(req).await?; self.client.check_response(&request_id, response).await?; @@ -1144,7 +1047,7 @@ impl Database for RemoteDatabase { } async fn describe_secret(&self, name: &str, namespace_path: &[String]) -> Result { - let secret_id = self.secret_id(name, namespace_path)?; + let secret_id = build_secret_identifier(name, namespace_path, &self.client.id_delimiter)?; let req = self .client .post(&format!("/v1/secret/{secret_id}/describe")); @@ -3486,38 +3389,6 @@ mod tests { } } - /// The default delimiter is outside the character set, so a component can - /// never hold one. A configured delimiter can be anything, and the readable - /// choices are all inside it -- under `.` the component `prod.vision` and - /// the pair `prod`, `vision` would join to one identifier. - #[tokio::test] - async fn test_a_configured_delimiter_is_refused_inside_a_component() { - use std::sync::{Arc, Mutex}; - let reached = Arc::new(Mutex::new(false)); - let flag = reached.clone(); - let conn = Connection::new_with_handler_and_config( - move |_| { - *flag.lock().unwrap() = true; - http::Response::builder().status(200).body("{}").unwrap() - }, - ClientConfig { - id_delimiter: Some(".".to_string()), - ..Default::default() - }, - ); - let error = conn - .drop_secret("openai", &["prod.vision".to_string()]) - .await - .expect_err("a component holding the delimiter must be refused"); - assert!(error.to_string().contains("delimiter"), "{error}"); - assert!(!*reached.lock().unwrap(), "the request was sent anyway"); - - // The same two names as separate segments are the ordinary case. - conn.drop_secret("openai", &["prod".to_string(), "vision".to_string()]) - .await - .unwrap(); - } - /// An identifier the service accepts is untouched by the encoding, so the /// route reads like the table and Function routes beside it. #[tokio::test] @@ -3587,32 +3458,6 @@ mod tests { } } - /// Under a configured delimiter that is inside the name charset, the one - /// segment `prod.vision` and the pair `prod`, `vision` would join to one - /// identifier. Refusing the first is what keeps them distinct. - #[tokio::test] - async fn test_a_configured_delimiter_is_refused_inside_a_table_component() { - use std::sync::{Arc, Mutex}; - let reached = Arc::new(Mutex::new(false)); - let flag = reached.clone(); - let conn = Connection::new_with_handler_and_config( - move |_| { - *flag.lock().unwrap() = true; - http::Response::builder().status(200).body("{}").unwrap() - }, - ClientConfig { - id_delimiter: Some(".".to_string()), - ..Default::default() - }, - ); - let error = conn - .drop_table("t", &["prod.vision".to_string()]) - .await - .expect_err("a segment holding the delimiter must be refused"); - assert!(error.to_string().contains("delimiter"), "{error}"); - assert!(!*reached.lock().unwrap(), "the request was sent anyway"); - } - /// A Function name is percent-encoded, which covers everything but the /// relative segment: `..` is unreserved, so it survives encoding and is /// then resolved away, posting a registration body to `/v1/create`. @@ -3662,6 +3507,57 @@ mod tests { } } + #[tokio::test] + async fn test_a_configured_delimiter_is_refused_inside_a_component() { + use std::sync::{Arc, Mutex}; + let reached = Arc::new(Mutex::new(false)); + let flag = reached.clone(); + let conn = Connection::new_with_handler_and_config( + move |_| { + *flag.lock().unwrap() = true; + http::Response::builder().status(200).body("{}").unwrap() + }, + ClientConfig { + id_delimiter: Some(".".to_string()), + ..Default::default() + }, + ); + let error = conn + .drop_secret("openai", &["prod.vision".to_string()]) + .await + .expect_err("a component holding the delimiter must be refused"); + assert!(error.to_string().contains("delimiter"), "{error}"); + assert!(!*reached.lock().unwrap(), "the request was sent anyway"); + + // The same two names as separate segments are the ordinary case. + conn.drop_secret("openai", &["prod".to_string(), "vision".to_string()]) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_a_configured_delimiter_is_refused_inside_a_table_component() { + use std::sync::{Arc, Mutex}; + let reached = Arc::new(Mutex::new(false)); + let flag = reached.clone(); + let conn = Connection::new_with_handler_and_config( + move |_| { + *flag.lock().unwrap() = true; + http::Response::builder().status(200).body("{}").unwrap() + }, + ClientConfig { + id_delimiter: Some(".".to_string()), + ..Default::default() + }, + ); + let error = conn + .drop_table("t", &["prod.vision".to_string()]) + .await + .expect_err("a segment holding the delimiter must be refused"); + assert!(error.to_string().contains("delimiter"), "{error}"); + assert!(!*reached.lock().unwrap(), "the request was sent anyway"); + } + #[tokio::test] async fn test_create_and_alter_secret_send_the_value_in_the_request_body() { for (route, call) in [ diff --git a/rust/lancedb/src/utils/mod.rs b/rust/lancedb/src/utils/mod.rs index 6e50472bf..96fe43eab 100644 --- a/rust/lancedb/src/utils/mod.rs +++ b/rust/lancedb/src/utils/mod.rs @@ -22,20 +22,10 @@ use datafusion_physical_plan::SendableRecordBatchStream; /// The characters any object name may contain: a table, a namespace segment, a /// Secret, a materialized view. /// -/// One set rather than one per object type. They were separate and identical, -/// which is worse than either having one or having a reason to differ -- a -/// reader had to compare them to find out, and they could drift without anyone -/// noticing. -/// -/// No positional rule on top of it: a name may begin with `_`, `-` or `.`, -/// because LanceDB namespaces already do, and anything narrower would put -/// objects out of reach inside namespaces that already exist. `.` and `..` are -/// excluded separately, by [`reject_relative_segment`], because that is a -/// property of where a name sits in a URL rather than of the name itself. -/// -/// No length bound. How long a name may be is the service's to decide, and a -/// bound here could only disagree with it -- one that is shorter refuses names -/// the catalog would hold, and one that is longer says nothing. +/// No positional rule on top of it -- a name may begin with `_`, `-` or `.`, +/// as LanceDB namespaces already do. `.` and `..` are excluded separately, by +/// [`reject_relative_segment`]: that is a property of where a name sits in a +/// URL, not of the name. Length is the service's to bound. static OBJECT_NAME_REGEX: std::sync::LazyLock = std::sync::LazyLock::new(|| regex::Regex::new(r"^[A-Za-z0-9_.\-]+$").unwrap()); @@ -96,123 +86,85 @@ impl PatchReadParam for ReadParams { } } +/// The reason `.` and `..` are refused wherever a name becomes a path segment. +const RELATIVE_SEGMENT_REASON: &str = + "'.' and '..' are read as relative path segments and cannot address an object"; + +/// Whether URL parsing would resolve this component away rather than keep it. +/// +/// Exactly `.` and `..`, and their percent-encoded spellings -- resolution +/// happens after decoding, so `%2E%2E` collapses as surely as `..` does, and +/// `drop_table("..")` would reach `/v1/drop/`. No wider than that: `...` is an +/// ordinary segment that addresses fine. +fn is_relative_segment(value: &str) -> bool { + let decoded = value.replace("%2e", ".").replace("%2E", "."); + decoded == "." || decoded == ".." +} + /// Refuse a path component that URL parsing resolves as a relative segment. /// -/// Exactly `.` and `..`, and the percent-encoded spellings of them. Those are -/// the only segments the URL Standard resolves, and it resolves them *after* -/// decoding -- `%2E%2E` is a double-dot segment as surely as `..` is, so no -/// escaping of one survives to reach a route. An object so named could be -/// created and then never addressed again, and on the way there the request -/// goes somewhere else: `drop_table("..")` resolves to `/v1/drop/`, delivering -/// a body meant for one route to whatever handler is left at the other. -/// -/// Deliberately no wider than that. `...` is an ordinary segment and addresses -/// perfectly well; refusing it would make an object that works today stop -/// working on upgrade, for a hazard it does not have. -fn reject_relative_segment(what: &str, value: &str) -> Result<()> { - let decoded = value.replace("%2e", ".").replace("%2E", "."); - if decoded == "." || decoded == ".." { +/// Reachable on its own for an identifier with no other validator to hang it +/// on: a Function name has no client-side grammar, so this is the only one of +/// these rules that applies to it. +pub(crate) fn reject_relative_segment(what: &str, value: &str) -> Result<()> { + if is_relative_segment(value) { return Err(Error::InvalidInput { - message: format!( - "invalid {what} '{value}': '.' and '..' are read as relative path segments and \ - cannot address an object" - ), + message: format!("invalid {what} '{value}': {RELATIVE_SEGMENT_REASON}"), }); } Ok(()) } -/// Validate table name. +/// Every rule an object name obeys, in one place: non-empty, inside +/// [`OBJECT_NAME_REGEX`], and addressable as a path segment. +/// +/// Returns the reason rather than an [`Error`], because the error *type* is +/// each API's own -- a table reports [`Error::InvalidTableName`] and the rest +/// report [`Error::InvalidInput`]. Sharing the rules but not the error is what +/// keeps a table, a namespace segment and a Secret from drifting apart. +fn check_object_name(name: &str) -> std::result::Result<(), &'static str> { + if name.is_empty() { + return Err("it must not be empty"); + } + if !OBJECT_NAME_REGEX.is_match(name) { + return Err( + "it may contain only alphanumeric characters, underscores, hyphens and periods", + ); + } + if is_relative_segment(name) { + return Err(RELATIVE_SEGMENT_REASON); + } + Ok(()) +} + +/// Validate a table name. pub fn validate_table_name(name: &str) -> Result<()> { - if name.is_empty() { - return Err(Error::InvalidTableName { - name: name.to_string(), - reason: "Table names cannot be empty strings".to_string(), - }); - } - if !OBJECT_NAME_REGEX.is_match(name) { - return Err(Error::InvalidTableName { - name: name.to_string(), - reason: - "Table names can only contain alphanumeric characters, underscores, hyphens, and periods" - .to_string(), - }); - } - reject_relative_segment("table name", name)?; - Ok(()) + check_object_name(name).map_err(|reason| Error::InvalidTableName { + name: name.to_string(), + reason: reason.to_string(), + }) } -/// Validate a namespace name component -/// -/// Namespace names must: -/// - Not be empty -/// - Only contain alphanumeric characters, underscores, hyphens, and periods -/// -/// # Arguments -/// * `name` - A single namespace component (not the full path) -/// -/// # Returns -/// * `Ok(())` if the namespace name is valid -/// * `Err(Error)` if the namespace name is invalid +/// Validate one component of a namespace path -- a single segment, not the +/// whole path. [`validate_namespace`] covers a path. pub fn validate_namespace_name(name: &str) -> Result<()> { - if name.is_empty() { - return Err(Error::InvalidInput { - message: "Namespace names cannot be empty strings".to_string(), - }); - } - if !OBJECT_NAME_REGEX.is_match(name) { - return Err(Error::InvalidInput { - message: format!( - "Invalid namespace name '{}': Namespace names can only contain alphanumeric characters, underscores, hyphens, and periods", - name - ), - }); - } - reject_relative_segment("namespace name", name)?; - Ok(()) + check_object_name(name).map_err(|reason| Error::InvalidInput { + message: format!("invalid namespace name '{name}': {reason}"), + }) } /// Validate one component of a Secret identifier: a Secret name, or one segment /// of the namespace path holding it. /// -/// # Why this is checked here, and not only by the service -/// -/// A Secret is addressed as `POST /v1/secret/{id}/`, where `{id}` is the -/// namespace path and the name joined by a delimiter. The client performs that -/// join. The service can only validate the components the *split* produced, so -/// by the time it sees anything, the identity has already been decided -- and a -/// component carrying path or delimiter syntax decides it differently from what -/// the caller asked for. -/// -/// `"a$b"` is not a name this service accepts, but joined and split it reads as -/// the namespace `a` and the name `b`: a different Secret, one that may well -/// exist. A create files a credential under it and an alter overwrites the -/// credential already there. Neither is an error the service could have -/// returned, because it was never asked about `a$b`. -/// -/// So this is not a second opinion on whether a name is acceptable. It is the -/// step that makes the service's opinion reachable at all, and it belongs to -/// whoever builds the URL -- which is this client, for every binding, since -/// Python and Node both wrap it. -/// -/// [`validate_table_name`] is called on the local path for the same reason. -/// This is the remote path catching up. +/// The client joins these into `{id}`, and the join decides identity -- the +/// service only ever sees what the split produced. `"a$b"` is not a name the +/// service accepts, but joined and split it reads as the namespace `a` and the +/// name `b`, a different Secret that may already exist. So this is not a second +/// opinion on the name; it is what lets the service have one. pub fn validate_secret_component(what: &str, value: &str) -> Result<()> { - if value.is_empty() { - return Err(Error::InvalidInput { - message: format!("{what} must not be empty"), - }); - } - if !OBJECT_NAME_REGEX.is_match(value) { - return Err(Error::InvalidInput { - message: format!( - "invalid {what} '{value}': it may contain only alphanumeric characters, \ - underscores, hyphens and periods" - ), - }); - } - reject_relative_segment(what, value)?; - Ok(()) + check_object_name(value).map_err(|reason| Error::InvalidInput { + message: format!("invalid {what} '{value}': {reason}"), + }) } /// Validate a Secret name and every segment of the namespace path holding it.