use std::sync::Arc;
use anyhow::{Context, Result};
use object_store::path::Path as ObjPath;
use object_store::ObjectStore;
pub fn connect(uri: &str) -> lancedb::connection::ConnectBuilder {
lancedb::connect(uri)
}
pub fn open(uri: &str) -> Result<(Arc<dyn ObjectStore>, ObjPath)> {
let owned = uri.to_string();
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
let (store, path) = rt.block_on(lance_io::object_store::ObjectStore::from_uri(&owned))?;
anyhow::Ok((store.inner.clone(), path))
})
.join()
.map_err(|_| anyhow::anyhow!("opening {uri} failed"))?
.with_context(|| format!("open {uri}"))
}
pub fn without_userinfo(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(i) = rest.find("://") {
let (head, tail) = rest.split_at(i + 3);
out.push_str(head);
let end = tail
.find(|c: char| c == '/' || c.is_whitespace())
.unwrap_or(tail.len());
match tail[..end].rfind('@') {
Some(at) => rest = &tail[at + 1..],
None => rest = tail,
}
}
out.push_str(rest);
out
}
pub fn explain(raw: &str) -> String {
if raw.contains("does not support timezone offsets") {
return "The AWS SSO token cache has an expiry time the AWS SDK cannot read; sign in again \
(`aws sso login`)"
.into();
}
if raw.contains("Failed to get AWS credentials")
|| raw.contains("CredentialsNotLoaded")
|| raw.contains("no credentials")
{
return "No storage credentials: run comindly where the platform's usual credentials are \
available (an IAM or workload role, AWS_PROFILE, GOOGLE_APPLICATION_CREDENTIALS, \
AZURE_STORAGE_ACCOUNT_NAME …)"
.into();
}
let code = raw
.split("<Code>")
.nth(1)
.and_then(|r| r.split("</Code>").next())
.map(str::to_string);
let hint = match code.as_deref() {
Some("ExpiredToken" | "TokenRefreshRequired") => {
Some("the storage credentials have expired; renew them and try again")
}
Some("AccessDenied" | "AuthorizationFailure" | "AuthorizationPermissionMismatch") => Some(
"the storage service denied access; check this machine's permissions on the bucket",
),
Some("NoSuchBucket" | "ContainerNotFound") => {
Some("the bucket does not exist (check the name and region)")
}
Some(
"InvalidAccessKeyId"
| "SignatureDoesNotMatch"
| "InvalidToken"
| "AuthenticationFailed",
) => Some("the storage service rejected the credentials"),
_ => None,
};
match (hint, code) {
(Some(h), Some(c)) => format!("{h} ({c})"),
_ => redact(raw),
}
}
fn redact(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut word = String::new();
let flush = |w: &mut String, out: &mut String| {
let has = |f: fn(&char) -> bool| w.chars().any(|c| f(&c));
let path = w.starts_with('/') || w.starts_with('~');
let secretish = !path
&& w.len() >= 60
&& has(char::is_ascii_uppercase)
&& has(char::is_ascii_lowercase)
&& has(char::is_ascii_digit);
out.push_str(if secretish { "[redacted]" } else { w.as_str() });
w.clear();
};
for c in s.chars() {
if c.is_ascii_alphanumeric() || "+/=_-%".contains(c) {
word.push(c);
} else {
flush(&mut word, &mut out);
out.push(c);
}
}
flush(&mut word, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drops_credentials_from_urls() {
assert_eq!(
without_userinfo("fatal: https://oauth2:abc123@gitlab.example.com/a/b.git not found"),
"fatal: https://gitlab.example.com/a/b.git not found"
);
assert_eq!(
without_userinfo("git@github.com:a/b.git"),
"git@github.com:a/b.git"
);
assert_eq!(
without_userinfo("https://github.com/a/b"),
"https://github.com/a/b"
);
}
#[test]
fn explains_s3_replies_without_secrets() {
let token = &"ExampleSessionToken0123456789".repeat(3);
let raw = format!(
"ListRequest: <Error><Code>ExpiredToken</Code><Token-0>{token}</Token-0></Error>"
);
let e = explain(&raw);
assert!(e.contains("(ExpiredToken)") && !e.contains(token), "{e}");
let other = explain(&format!("connect failed: token={token} at s3://bucket/x"));
assert!(
!other.contains(token) && other.contains("s3://bucket/x"),
"{other}"
);
let path = format!("/home/{}", "long-folder-name-".repeat(6));
assert_eq!(explain(&path), path);
let offset = "Failed to get AWS credentials: InvalidField { field: \"expiresAt\", source: DateTimeParseError { kind: Invalid(\"Smithy does not support timezone offsets in RFC-3339 date times\") } }";
assert!(explain(offset).contains("aws sso login"));
let chain = "Failed to get AWS credentials: CredentialsNotLoaded(CredentialsNotLoaded { source: Some(ProviderChainError { attempts: [] }) })";
assert!(
explain(chain).starts_with("No storage credentials"),
"{}",
explain(chain)
);
let mixed = "/Users/Someone/Library/Caches/tmp-Ab12Cd34Ef56/index/_graph/symbols.lance";
assert_eq!(explain(mixed), mixed);
}
}