Files
rustfs/crates/utils/src/string.rs
Nugine 2525b66658 refactor: replace lazy_static with LazyLock (#164)
* refactor: replace `lazy_static` with `LazyLock`

* update cursorrules

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-07-10 23:50:46 +08:00

715 lines
21 KiB
Rust

// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use rand::{Rng, RngCore};
use regex::Regex;
use std::io::{Error, Result};
use std::sync::LazyLock;
pub fn parse_bool(str: &str) -> Result<bool> {
match str {
"1" | "t" | "T" | "true" | "TRUE" | "True" | "on" | "ON" | "On" | "enabled" => Ok(true),
"0" | "f" | "F" | "false" | "FALSE" | "False" | "off" | "OFF" | "Off" | "disabled" => Ok(false),
_ => Err(Error::other(format!("ParseBool: parsing {str}"))),
}
}
pub fn match_simple(pattern: &str, name: &str) -> bool {
if pattern.is_empty() {
return name == pattern;
}
if pattern == "*" {
return true;
}
// Do an extended wildcard '*' and '?' match.
deep_match_rune(name.as_bytes(), pattern.as_bytes(), true)
}
pub fn match_pattern(pattern: &str, name: &str) -> bool {
if pattern.is_empty() {
return name == pattern;
}
if pattern == "*" {
return true;
}
// Do an extended wildcard '*' and '?' match.
deep_match_rune(name.as_bytes(), pattern.as_bytes(), false)
}
pub fn has_pattern(patterns: &[&str], match_str: &str) -> bool {
for pattern in patterns {
if match_simple(pattern, match_str) {
return true;
}
}
false
}
pub fn has_string_suffix_in_slice(str: &str, list: &[&str]) -> bool {
let str = str.to_lowercase();
for v in list {
if *v == "*" {
return true;
}
if str.ends_with(&v.to_lowercase()) {
return true;
}
}
false
}
fn deep_match_rune(str_: &[u8], pattern: &[u8], simple: bool) -> bool {
let (mut str_, mut pattern) = (str_, pattern);
while !pattern.is_empty() {
match pattern[0] as char {
'*' => {
return if pattern.len() == 1 {
true
} else {
deep_match_rune(str_, &pattern[1..], simple)
|| (!str_.is_empty() && deep_match_rune(&str_[1..], pattern, simple))
};
}
'?' => {
if str_.is_empty() {
return simple;
}
}
_ => {
if str_.is_empty() || str_[0] != pattern[0] {
return false;
}
}
}
str_ = &str_[1..];
pattern = &pattern[1..];
}
str_.is_empty() && pattern.is_empty()
}
pub fn match_as_pattern_prefix(pattern: &str, text: &str) -> bool {
let mut i = 0;
while i < text.len() && i < pattern.len() {
match pattern.as_bytes()[i] as char {
'*' => return true,
'?' => i += 1,
_ => {
if pattern.as_bytes()[i] != text.as_bytes()[i] {
return false;
}
}
}
i += 1;
}
text.len() <= pattern.len()
}
static ELLIPSES_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(.*)(\{[0-9a-z]*\.\.\.[0-9a-z]*\})(.*)").unwrap());
/// Ellipses constants
const OPEN_BRACES: &str = "{";
const CLOSE_BRACES: &str = "}";
const ELLIPSES: &str = "...";
/// ellipses pattern, describes the range and also the
/// associated prefix and suffixes.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Pattern {
pub prefix: String,
pub suffix: String,
pub seq: Vec<String>,
}
impl Pattern {
/// expands a ellipses pattern.
pub fn expand(&self) -> Vec<String> {
let mut ret = Vec::with_capacity(self.suffix.len());
for v in self.seq.iter() {
match (self.prefix.is_empty(), self.suffix.is_empty()) {
(false, true) => ret.push(format!("{}{}", self.prefix, v)),
(true, false) => ret.push(format!("{}{}", v, self.suffix)),
(true, true) => ret.push(v.to_string()),
(false, false) => ret.push(format!("{}{}{}", self.prefix, v, self.suffix)),
}
}
ret
}
pub fn len(&self) -> usize {
self.seq.len()
}
pub fn is_empty(&self) -> bool {
self.seq.is_empty()
}
}
/// contains a list of patterns provided in the input.
#[derive(Debug, PartialEq, Eq)]
pub struct ArgPattern {
inner: Vec<Pattern>,
}
impl AsRef<Vec<Pattern>> for ArgPattern {
fn as_ref(&self) -> &Vec<Pattern> {
&self.inner
}
}
impl AsMut<Vec<Pattern>> for ArgPattern {
fn as_mut(&mut self) -> &mut Vec<Pattern> {
&mut self.inner
}
}
impl ArgPattern {
pub fn new(inner: Vec<Pattern>) -> Self {
Self { inner }
}
/// expands all the ellipses patterns in the given argument.
pub fn expand(&self) -> Vec<Vec<String>> {
let ret: Vec<Vec<String>> = self.inner.iter().map(|v| v.expand()).collect();
Self::arg_expander(&ret)
}
/// recursively expands labels into its respective forms.
fn arg_expander(lbs: &[Vec<String>]) -> Vec<Vec<String>> {
if lbs.len() == 1 {
return lbs[0].iter().map(|v| vec![v.to_string()]).collect();
}
let mut ret = Vec::new();
let (first, others) = lbs.split_at(1);
for bs in first[0].iter() {
let ots = Self::arg_expander(others);
for mut obs in ots {
obs.push(bs.to_string());
ret.push(obs);
}
}
ret
}
/// returns the total number of sizes in the given patterns.
pub fn total_sizes(&self) -> usize {
self.inner.iter().fold(1, |acc, v| acc * v.seq.len())
}
}
/// finds all ellipses patterns, recursively and parses the ranges numerically.
pub fn find_ellipses_patterns(arg: &str) -> Result<ArgPattern> {
let mut parts = match ELLIPSES_RE.captures(arg) {
Some(caps) => caps,
None => {
return Err(Error::other(format!(
"Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are positive integers, M must be greater than N, with an allowed minimum range of 4"
)));
}
};
let mut patterns = Vec::new();
while let Some(prefix) = parts.get(1) {
let seq = parse_ellipses_range(parts[2].into())?;
match ELLIPSES_RE.captures(prefix.into()) {
Some(cs) => {
patterns.push(Pattern {
seq,
prefix: String::new(),
suffix: parts[3].into(),
});
parts = cs;
}
None => {
patterns.push(Pattern {
seq,
prefix: prefix.as_str().to_owned(),
suffix: parts[3].into(),
});
break;
}
};
}
// Check if any of the prefix or suffixes now have flower braces
// left over, in such a case we generally think that there is
// perhaps a typo in users input and error out accordingly.
for p in patterns.iter() {
if p.prefix.contains(OPEN_BRACES)
|| p.prefix.contains(CLOSE_BRACES)
|| p.suffix.contains(OPEN_BRACES)
|| p.suffix.contains(CLOSE_BRACES)
{
return Err(Error::other(format!(
"Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are positive integers, M must be greater than N, with an allowed minimum range of 4"
)));
}
}
Ok(ArgPattern::new(patterns))
}
/// returns true if input arg has ellipses type pattern.
pub fn has_ellipses<T: AsRef<str>>(s: &[T]) -> bool {
let pattern = [ELLIPSES, OPEN_BRACES, CLOSE_BRACES];
s.iter().any(|v| pattern.iter().any(|p| v.as_ref().contains(p)))
}
/// Parses an ellipses range pattern of following style
///
/// example:
/// {1...64}
/// {33...64}
pub fn parse_ellipses_range(pattern: &str) -> Result<Vec<String>> {
if !pattern.contains(OPEN_BRACES) {
return Err(Error::other("Invalid argument"));
}
if !pattern.contains(CLOSE_BRACES) {
return Err(Error::other("Invalid argument"));
}
let ellipses_range: Vec<&str> = pattern
.trim_start_matches(OPEN_BRACES)
.trim_end_matches(CLOSE_BRACES)
.split(ELLIPSES)
.collect();
if ellipses_range.len() != 2 {
return Err(Error::other("Invalid argument"));
}
// TODO: Add support for hexadecimals.
let start = ellipses_range[0].parse::<usize>().map_err(Error::other)?;
let end = ellipses_range[1].parse::<usize>().map_err(Error::other)?;
if start > end {
return Err(Error::other("Invalid argument:range start cannot be bigger than end"));
}
let mut ret: Vec<String> = Vec::with_capacity(end - start + 1);
for i in start..=end {
if ellipses_range[0].starts_with('0') && ellipses_range[0].len() > 1 {
ret.push(format!("{:0width$}", i, width = ellipses_range[1].len()));
} else {
ret.push(format!("{i}"));
}
}
Ok(ret)
}
pub fn gen_access_key(length: usize) -> Result<String> {
const ALPHA_NUMERIC_TABLE: [char; 36] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
if length < 3 {
return Err(Error::other("access key length is too short"));
}
let mut result = String::with_capacity(length);
let mut rng = rand::rng();
for _ in 0..length {
result.push(ALPHA_NUMERIC_TABLE[rng.random_range(0..ALPHA_NUMERIC_TABLE.len())]);
}
Ok(result)
}
pub fn gen_secret_key(length: usize) -> Result<String> {
use base64_simd::URL_SAFE_NO_PAD;
if length < 8 {
return Err(Error::other("secret key length is too short"));
}
let mut rng = rand::rng();
let mut key = vec![0u8; URL_SAFE_NO_PAD.estimated_decoded_length(length)];
rng.fill_bytes(&mut key);
let encoded = URL_SAFE_NO_PAD.encode_to_string(&key);
let key_str = encoded.replace("/", "+");
Ok(key_str)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_ellipses() {
// Tests for all args without ellipses.
let test_cases = [
(1, vec!["64"], false),
// Found flower braces, still attempt to parse and throw an error.
(2, vec!["{1..64}"], true),
(3, vec!["{1..2..}"], true),
// Test for valid input.
(4, vec!["1...64"], true),
(5, vec!["{1...2O}"], true),
(6, vec!["..."], true),
(7, vec!["{-1...1}"], true),
(8, vec!["{0...-1}"], true),
(9, vec!["{1....4}"], true),
(10, vec!["{1...64}"], true),
(11, vec!["{...}"], true),
(12, vec!["{1...64}", "{65...128}"], true),
(13, vec!["http://rustfs{2...3}/export/set{1...64}"], true),
(
14,
vec![
"http://rustfs{2...3}/export/set{1...64}",
"http://rustfs{2...3}/export/set{65...128}",
],
true,
),
(15, vec!["mydisk-{a...z}{1...20}"], true),
(16, vec!["mydisk-{1...4}{1..2.}"], true),
];
for (i, args, expected) in test_cases {
let ret = has_ellipses(&args);
assert_eq!(ret, expected, "Test{i}: Expected {expected}, got {ret}");
}
}
#[test]
fn test_find_ellipses_patterns() {
#[derive(Default)]
struct TestCase<'a> {
num: usize,
pattern: &'a str,
success: bool,
want: Vec<Vec<&'a str>>,
}
let test_cases = [
TestCase {
num: 1,
pattern: "{1..64}",
..Default::default()
},
TestCase {
num: 2,
pattern: "1...64",
..Default::default()
},
TestCase {
num: 2,
pattern: "...",
..Default::default()
},
TestCase {
num: 3,
pattern: "{1...",
..Default::default()
},
TestCase {
num: 4,
pattern: "...64}",
..Default::default()
},
TestCase {
num: 5,
pattern: "{...}",
..Default::default()
},
TestCase {
num: 6,
pattern: "{-1...1}",
..Default::default()
},
TestCase {
num: 7,
pattern: "{0...-1}",
..Default::default()
},
TestCase {
num: 8,
pattern: "{1...2O}",
..Default::default()
},
TestCase {
num: 9,
pattern: "{64...1}",
..Default::default()
},
TestCase {
num: 10,
pattern: "{1....4}",
..Default::default()
},
TestCase {
num: 11,
pattern: "mydisk-{a...z}{1...20}",
..Default::default()
},
TestCase {
num: 12,
pattern: "mydisk-{1...4}{1..2.}",
..Default::default()
},
TestCase {
num: 13,
pattern: "{1..2.}-mydisk-{1...4}",
..Default::default()
},
TestCase {
num: 14,
pattern: "{{1...4}}",
..Default::default()
},
TestCase {
num: 16,
pattern: "{4...02}",
..Default::default()
},
TestCase {
num: 17,
pattern: "{f...z}",
..Default::default()
},
// Test for valid input.
TestCase {
num: 18,
pattern: "{1...64}",
success: true,
want: vec![
vec!["1"],
vec!["2"],
vec!["3"],
vec!["4"],
vec!["5"],
vec!["6"],
vec!["7"],
vec!["8"],
vec!["9"],
vec!["10"],
vec!["11"],
vec!["12"],
vec!["13"],
vec!["14"],
vec!["15"],
vec!["16"],
vec!["17"],
vec!["18"],
vec!["19"],
vec!["20"],
vec!["21"],
vec!["22"],
vec!["23"],
vec!["24"],
vec!["25"],
vec!["26"],
vec!["27"],
vec!["28"],
vec!["29"],
vec!["30"],
vec!["31"],
vec!["32"],
vec!["33"],
vec!["34"],
vec!["35"],
vec!["36"],
vec!["37"],
vec!["38"],
vec!["39"],
vec!["40"],
vec!["41"],
vec!["42"],
vec!["43"],
vec!["44"],
vec!["45"],
vec!["46"],
vec!["47"],
vec!["48"],
vec!["49"],
vec!["50"],
vec!["51"],
vec!["52"],
vec!["53"],
vec!["54"],
vec!["55"],
vec!["56"],
vec!["57"],
vec!["58"],
vec!["59"],
vec!["60"],
vec!["61"],
vec!["62"],
vec!["63"],
vec!["64"],
],
},
TestCase {
num: 19,
pattern: "{1...5} {65...70}",
success: true,
want: vec![
vec!["1 ", "65"],
vec!["2 ", "65"],
vec!["3 ", "65"],
vec!["4 ", "65"],
vec!["5 ", "65"],
vec!["1 ", "66"],
vec!["2 ", "66"],
vec!["3 ", "66"],
vec!["4 ", "66"],
vec!["5 ", "66"],
vec!["1 ", "67"],
vec!["2 ", "67"],
vec!["3 ", "67"],
vec!["4 ", "67"],
vec!["5 ", "67"],
vec!["1 ", "68"],
vec!["2 ", "68"],
vec!["3 ", "68"],
vec!["4 ", "68"],
vec!["5 ", "68"],
vec!["1 ", "69"],
vec!["2 ", "69"],
vec!["3 ", "69"],
vec!["4 ", "69"],
vec!["5 ", "69"],
vec!["1 ", "70"],
vec!["2 ", "70"],
vec!["3 ", "70"],
vec!["4 ", "70"],
vec!["5 ", "70"],
],
},
TestCase {
num: 20,
pattern: "{01...036}",
success: true,
want: vec![
vec!["001"],
vec!["002"],
vec!["003"],
vec!["004"],
vec!["005"],
vec!["006"],
vec!["007"],
vec!["008"],
vec!["009"],
vec!["010"],
vec!["011"],
vec!["012"],
vec!["013"],
vec!["014"],
vec!["015"],
vec!["016"],
vec!["017"],
vec!["018"],
vec!["019"],
vec!["020"],
vec!["021"],
vec!["022"],
vec!["023"],
vec!["024"],
vec!["025"],
vec!["026"],
vec!["027"],
vec!["028"],
vec!["029"],
vec!["030"],
vec!["031"],
vec!["032"],
vec!["033"],
vec!["034"],
vec!["035"],
vec!["036"],
],
},
TestCase {
num: 21,
pattern: "{001...036}",
success: true,
want: vec![
vec!["001"],
vec!["002"],
vec!["003"],
vec!["004"],
vec!["005"],
vec!["006"],
vec!["007"],
vec!["008"],
vec!["009"],
vec!["010"],
vec!["011"],
vec!["012"],
vec!["013"],
vec!["014"],
vec!["015"],
vec!["016"],
vec!["017"],
vec!["018"],
vec!["019"],
vec!["020"],
vec!["021"],
vec!["022"],
vec!["023"],
vec!["024"],
vec!["025"],
vec!["026"],
vec!["027"],
vec!["028"],
vec!["029"],
vec!["030"],
vec!["031"],
vec!["032"],
vec!["033"],
vec!["034"],
vec!["035"],
vec!["036"],
],
},
];
for test_case in test_cases {
let ret = find_ellipses_patterns(test_case.pattern);
match ret {
Ok(v) => {
if !test_case.success {
panic!("Test{}: Expected failure but passed instead", test_case.num);
}
let got = v.expand();
if got.len() != test_case.want.len() {
panic!("Test{}: Expected {}, got {}", test_case.num, test_case.want.len(), got.len());
}
assert_eq!(got, test_case.want, "Test{}: Expected {:?}, got {:?}", test_case.num, test_case.want, got);
}
Err(e) => {
if test_case.success {
panic!("Test{}: Expected success but failed instead {:?}", test_case.num, e);
}
}
}
}
}
}