use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use crate::braceexpand::braceexpand;
use crate::error::{Error, Result};
use crate::prelude::*;
pub fn base_plus_ext(path: &str) -> Option<(&str, &str)> {
let start = path.rfind('/').map(|at| at + 1).unwrap_or(0);
let dot = start + path[start..].find('.')?;
if dot == start {
return None;
}
Some((&path[..dot], &path[dot + 1..]))
}
pub fn is_shard_metadata(name: &str) -> bool {
if !name.contains('/') && name.starts_with("__") && name.ends_with("__") {
return true;
}
let Some(rest) = name.strip_prefix("__") else {
return false;
};
let Some(end) = rest.find("__") else {
return false;
};
if rest[..end].contains('/') {
return false;
}
let after = &rest[end + 2..];
after.is_empty() || after.starts_with('/')
}
#[cfg(feature = "std")]
pub fn envsubst(text: &str) -> Result<String> {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find("${") {
out.push_str(&rest[..at]);
let body = &rest[at + 2..];
let Some(end) = body.find('}') else {
out.push_str(&rest[at..]);
return Ok(out);
};
let name = &body[..end];
if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
out.push_str(&rest[at..at + 2 + end + 1]);
rest = &body[end + 1..];
continue;
}
let variable = format!("WDS_{name}");
match std::env::var(&variable) {
Ok(value) => out.push_str(&value),
Err(_) => return Err(Error::value(format!("missing environment variable {variable}"))),
}
rest = &body[end + 1..];
}
out.push_str(rest);
Ok(out)
}
#[cfg(not(feature = "std"))]
pub fn envsubst(text: &str) -> Result<String> {
Ok(String::from(text))
}
pub fn expand_urls(spec: &str) -> Result<Vec<String>> {
let mut result = Vec::new();
for part in spec.split("::") {
let mut url = part.to_string();
for _ in 0..10 {
let next = envsubst(&url)?;
if next == url {
break;
}
url = next;
}
result.extend(braceexpand(&url)?);
}
Ok(result)
}
pub fn make_seed(parts: &[u64]) -> u64 {
let mut seed: u64 = 0;
for part in parts {
seed = seed.wrapping_mul(31).wrapping_add(mix(*part)) & 0x7FFF_FFFF;
}
seed
}
fn mix(mut x: u64) -> u64 {
x ^= x >> 33;
x = x.wrapping_mul(0xff51_afd7_ed55_8ccd);
x ^= x >> 33;
x = x.wrapping_mul(0xc4ce_b9fe_1a85_ec53);
x ^ (x >> 33)
}
pub fn seed_from_str(text: &str) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in text.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WorkerInfo {
pub rank: usize,
pub world_size: usize,
pub worker: usize,
pub num_workers: usize,
}
impl Default for WorkerInfo {
fn default() -> Self {
WorkerInfo { rank: 0, world_size: 1, worker: 0, num_workers: 1 }
}
}
impl WorkerInfo {
pub fn seed(&self) -> u64 {
(self.rank * 1000 + self.worker) as u64
}
}
pub fn worker_info() -> WorkerInfo {
let mut info = WorkerInfo::default();
#[cfg(feature = "std")]
if let (Ok(rank), Ok(world)) = (env_usize("RANK"), env_usize("WORLD_SIZE")) {
info.rank = rank;
info.world_size = world.max(1);
}
if let Some((worker, num_workers)) = crate::workers::current() {
info.worker = worker;
info.num_workers = num_workers.max(1);
return info;
}
#[cfg(feature = "std")]
if let (Ok(worker), Ok(num)) = (env_usize("WORKER"), env_usize("NUM_WORKERS")) {
info.worker = worker;
info.num_workers = num.max(1);
}
info
}
pub use crate::workers::{clear_worker, set_thread_id_hook, set_worker, with_worker};
#[cfg(feature = "std")]
fn env_usize(name: &str) -> core::result::Result<usize, ()> {
std::env::var(name).map_err(|_| ())?.parse().map_err(|_| ())
}
static SECURE: AtomicBool = AtomicBool::new(false);
static SECURE_INIT: AtomicBool = AtomicBool::new(false);
pub fn enforce_security() -> bool {
if !SECURE_INIT.swap(true, Ordering::Relaxed) {
#[cfg(feature = "std")]
{
let on = std::env::var("WDS_SECURE").map(|v| v != "0" && !v.is_empty()).unwrap_or(false);
SECURE.store(on, Ordering::Relaxed);
}
}
SECURE.load(Ordering::Relaxed)
}
pub fn set_enforce_security(on: bool) {
SECURE_INIT.store(true, Ordering::Relaxed);
SECURE.store(on, Ordering::Relaxed);
}
pub fn check_security(what: &str) -> Result<()> {
if enforce_security() {
return Err(Error::Security(what.to_string()));
}
Ok(())
}
static COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn next_unique() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
pub fn format_shard_pattern(pattern: &str, value: usize) -> Result<String> {
let mut out = String::with_capacity(pattern.len() + 8);
let mut chars = pattern.chars().peekable();
let mut substituted = false;
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
if chars.peek() == Some(&'%') {
chars.next();
out.push('%');
continue;
}
let mut width = String::new();
while let Some(&d) = chars.peek() {
if d.is_ascii_digit() {
width.push(d);
chars.next();
} else {
break;
}
}
match chars.next() {
Some('d') | Some('i') => {
let zero_padded = width.starts_with('0');
let w: usize = width.parse().unwrap_or(0);
if zero_padded {
out.push_str(&format!("{value:0>w$}"));
} else {
out.push_str(&format!("{value:>w$}"));
}
substituted = true;
}
other => {
return Err(Error::value(format!(
"shard pattern {pattern:?} has an unsupported conversion %{}",
other.unwrap_or(' ')
)));
}
}
}
if !substituted {
return Err(Error::value(format!("shard pattern {pattern:?} has no %d conversion")));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_names_the_way_the_format_requires() {
assert_eq!(base_plus_ext("a.png"), Some(("a", "png")));
assert_eq!(base_plus_ext("dir/a.seg.png"), Some(("dir/a", "seg.png")));
assert_eq!(base_plus_ext("a/b.c/d.png"), Some(("a/b.c/d", "png")));
assert_eq!(base_plus_ext("plain"), None);
}
#[test]
fn recognises_shard_metadata() {
assert!(is_shard_metadata("__index__"));
assert!(is_shard_metadata("__dup__/a.png"));
assert!(!is_shard_metadata("dir/__index__"));
assert!(!is_shard_metadata("normal.png"));
}
#[test]
fn formats_shard_patterns() {
assert_eq!(format_shard_pattern("s-%06d.tar", 7).unwrap(), "s-000007.tar");
assert_eq!(format_shard_pattern("s-%d.tar", 7).unwrap(), "s-7.tar");
assert_eq!(format_shard_pattern("100%%-%d", 1).unwrap(), "100%-1");
assert!(format_shard_pattern("no-conversion.tar", 1).is_err());
assert!(format_shard_pattern("%s.tar", 1).is_err());
}
#[test]
fn overrides_worker_identity_per_thread() {
let info = with_worker(2, 4, worker_info);
assert_eq!(info.worker, 2);
assert_eq!(info.num_workers, 4);
assert_eq!(worker_info().worker, 0, "override is scoped to the closure");
}
#[test]
fn derives_stable_distinct_seeds() {
assert_eq!(make_seed(&[1, 2, 3]), make_seed(&[1, 2, 3]));
assert_ne!(make_seed(&[1, 2, 3]), make_seed(&[1, 2, 4]));
assert!(make_seed(&[u64::MAX, 7]) <= 0x7FFF_FFFF);
}
}