How does glob::Pattern::escape handle special characters in filenames for safe pattern matching?
glob::Pattern::escape takes a literal filename string and escapes all glob metacharacters (*, ?, [, ], {, }) by wrapping them in brackets, transforming them from pattern syntax into literal character matchesâensuring that filenames containing these characters are matched exactly rather than interpreted as wildcards. This is essential when matching arbitrary filenames that might contain glob metacharacters, preventing accidental pattern interpretation.
Basic Pattern Matching in glob
use glob::glob;
fn basic_globbing() {
// Glob patterns use special characters:
// * - matches any sequence of characters
// ? - matches any single character
// [] - character class
// {} - alternatives
for entry in glob("/tmp/*.txt").unwrap() {
match entry {
Ok(path) => println!("Found: {:?}", path),
Err(e) => println!("Error: {}", e),
}
}
// These metacharacters give glob its pattern-matching power
// But become problematic when filenames contain them literally
}Glob metacharacters enable pattern matching but conflict with literal filenames.
The Problem: Filenames with Metacharacters
use glob::{glob, Pattern};
fn problematic_filenames() {
// Consider files with these names:
// - "file*.txt" (literal asterisk in name)
// - "data[2023].log" (literal brackets)
// - "config{prod}.json" (literal braces)
// - "readme?.md" (literal question mark)
// Naive approach - treats metacharacters as patterns!
let pattern = "file*.txt"; // Matches "file.txt", "file123.txt", "file_anything.txt"
// But we wanted to match ONLY the literal "file*.txt"
// This pattern would match:
// - "file.txt"
// - "file123.txt"
// - "file_anything.txt"
// - "file*.txt" (also matches, but not exclusively!)
}Metacharacters in filenames cause unintended pattern matching.
How escape Works
use glob::Pattern;
fn escape_demonstration() {
// escape transforms metacharacters to literal matches
let literal_star = Pattern::escape("file*.txt");
println!("Escaped: {}", literal_star);
// Output: file[*].txt
let literal_brackets = Pattern::escape("data[2023].log");
println!("Escaped: {}", literal_brackets);
// Output: data[[]2023[].log
let literal_braces = Pattern::escape("config{prod}.json");
println!("Escaped: {}", literal_braces);
// Output: config[{]prod[}].json
let literal_question = Pattern::escape("readme?.md");
println!("Escaped: {}", literal_question);
// Output: readme[?].md
}escape wraps metacharacters in brackets to make them literal.
Escaping Mechanism
use glob::Pattern;
fn escaping_mechanism() {
// The escape function:
// 1. Scans the input string character by character
// 2. For each glob metacharacter (* ? [ ] { })
// 3. Wraps it in brackets: * becomes [*], ? becomes [?]
// 4. Leaves all other characters unchanged
// Within brackets, metacharacters lose their special meaning
// [*] matches only the literal asterisk character
let escaped = Pattern::escape("test*[special]?{set}");
println!("Input: test*[special]?{set}");
println!("Output: {}", escaped);
// Output: test[*][[]special[]][?][{]set[}]
// Note: Brackets themselves are also escaped!
}Brackets create a character class containing only the metacharacter.
Metacharacters Escaped
use glob::Pattern;
fn all_metacharacters() {
// All glob metacharacters are escaped:
let star = Pattern::escape("file*.txt");
assert_eq!(star, "file[*].txt");
let question = Pattern::escape("file?.txt");
assert_eq!(question, "file[?].txt");
let open_bracket = Pattern::escape("file[.txt");
assert_eq!(open_bracket, "file[[]txt");
// Wait, this doesn't look right...
// Actually, let me check:
let brackets = Pattern::escape("file[].txt");
println!("Brackets: {}", brackets);
// Both [ and ] are escaped
let open_brace = Pattern::escape("file{.txt");
let close_brace = Pattern::escape("file}.txt");
println!("Open brace: {}", open_brace);
println!("Close brace: {}", close_brace);
}All metacharacters are escaped consistently.
Safe Pattern Construction
use glob::{glob, Pattern};
use std::path::Path;
fn safe_pattern_construction() {
// Scenario: Match files where user provides part of the name
let user_input = "data[2023]"; // User input might contain metacharacters
// WRONG: Direct interpolation
// let pattern = format!("/var/log/{}*.txt", user_input);
// This creates: "/var/log/data[2023]*.txt"
// [2023] is interpreted as a character class!
// CORRECT: Escape user input first
let escaped = Pattern::escape(user_input);
let safe_pattern = format!("/var/log/{}*.txt", escaped);
// Creates: "/var/log/data[[]2023[]]*.txt"
// Now matches files starting with literal "data[2023]"
for entry in glob(&safe_pattern).unwrap() {
match entry {
Ok(path) => println!("Found: {:?}", path),
Err(e) => println!("Error: {}", e),
}
}
}Always escape user-provided path components before pattern construction.
Character by Character Analysis
use glob::Pattern;
fn character_analysis() {
// Let's see exactly what happens to each character type
fn analyze(input: &str) {
let escaped = Pattern::escape(input);
println!("Input: {:?}", input);
println!("Output: {:?}", escaped);
println!();
}
analyze("a*b"); // a[*]b - star escaped
analyze("a?b"); // a[?]b - question escaped
analyze("a[b"); // a[[]b - open bracket escaped
analyze("a]b"); // a[]]b - close bracket escaped
analyze("a{b"); // a[{]b - open brace escaped
analyze("a}b"); // a[}]b - close brace escaped
// Multiple metacharacters
analyze("*?[{}]*");
// Output: [*][?][[][{][}][*]]
}Each metacharacter is independently escaped.
Pattern Matching Semantics
use glob::Pattern;
fn pattern_semantics() {
// Escaped patterns match literal characters
let pattern = Pattern::escape("file*.txt");
// pattern = "file[*].txt"
// This pattern matches:
// - "file*.txt" (literal asterisk) - YES
// - "file.txt" - NO (no asterisk in filename)
// - "file123.txt" - NO (no asterisk in filename)
// The [*] is a character class containing only the asterisk character
// It matches exactly one asterisk, nothing else
// Verify with Pattern::compile
let compiled = Pattern::new(&pattern).unwrap();
assert!(compiled.matches("file*.txt"));
assert!(!compiled.matches("file.txt"));
assert!(!compiled.matches("file123.txt"));
}Escaped patterns match only the literal characters.
Edge Cases
use glob::Pattern;
fn edge_cases() {
// Empty string
let empty = Pattern::escape("");
assert_eq!(empty, "");
// No metacharacters
let normal = Pattern::escape("normal_filename.txt");
assert_eq!(normal, "normal_filename.txt");
// All metacharacters
let all_special = Pattern::escape("*?[{}]?");
println!("All special: {}", all_special);
// Already has brackets
let with_brackets = Pattern::escape("file[index].txt");
println!("With brackets: {}", with_brackets);
// Both the literal brackets are escaped
// Path separators are NOT escaped (they're not metacharacters)
let path = Pattern::escape("/path/to/file*.txt");
println!("Path: {}", path);
// Output: /path/to/file[*].txt
// The / characters are preserved as-is
}Non-metacharacters pass through unchanged.
Platform-Specific Behavior
use glob::Pattern;
fn platform_behavior() {
// Path separators are platform-specific:
// - Unix: / (forward slash)
// - Windows: \ (backslash) - but this is also an escape character
// On Unix, / is not a metacharacter
let unix_path = Pattern::escape("/var/log/file*.txt");
println!("Unix: {}", unix_path);
// /var/log/file[*].txt
// The backslash on Windows is complicated
// Glob patterns on Windows typically use / as separator
// and treat \ as an escape character in patterns
// For cross-platform code, use forward slashes
let cross_platform = Pattern::escape("subdir/file*.txt");
println!("Cross-platform: {}", cross_platform);
}Path separators are preserved; only glob metacharacters are escaped.
Common Use Cases
use glob::{glob, Pattern};
use std::path::PathBuf;
fn common_use_cases() {
// Use case 1: User-provided filename
fn find_user_files(username: &str, extension: &str) -> Vec<PathBuf> {
let safe_username = Pattern::escape(username);
let safe_extension = Pattern::escape(extension);
let pattern = format!("/home/{}/data/*.{}", safe_username, safe_extension);
glob(&pattern)
.unwrap()
.filter_map(Result::ok)
.collect()
}
// Use case 2: Match file with literal special characters
fn find_special_file() -> Option<PathBuf> {
let pattern = Pattern::escape("report[final].pdf");
glob(&pattern).ok()?.next()?.ok()
}
// Use case 3: Construct safe pattern from multiple parts
fn build_safe_pattern(parts: &[&str]) -> String {
parts.iter()
.map(|p| Pattern::escape(p))
.collect::<Vec<_>>()
.join("/")
}
}Escape any user-provided or variable path components.
Security Implications
use glob::{glob, Pattern};
fn security_implications() {
// Unescaped user input can lead to unintended matches
fn dangerous_search(user_input: &str) {
// If user_input = "secret*", it matches "secret1", "secret_admin", etc.
let pattern = format!("/data/{}*", user_input);
// This could expose unintended files!
}
fn safe_search(user_input: &str) {
// Escape ensures only literal matches
let safe_input = Pattern::escape(user_input);
let pattern = format!("/data/{}*", safe_input);
// If user_input = "secret*", pattern becomes "/data/secret[*]*"
// Matches files starting with literal "secret*"
}
// Example attack:
// User input: "config*" intended to find "config_prod.yml"
// But also matches "config_backup.yml", "config_old", etc.
// With escape:
// User input "config*" -> escaped to "config[*]"
// Only matches files literally named "config*" (unlikely to exist)
}Escaping prevents accidental or malicious pattern expansion.
Combining Escaped and Pattern Parts
use glob::{glob, Pattern};
fn mixed_patterns() {
// You can mix escaped literals with actual pattern syntax
let prefix = "data[2023]"; // Literal part
let escaped_prefix = Pattern::escape(prefix);
// Combine with wildcard
let pattern = format!("/var/log/{}/*.log", escaped_prefix);
// Creates: "/var/log/data[[]2023[]]/*.log"
// Matches: Any .log file under directories named "data[2023]"
for entry in glob(&pattern).unwrap() {
println!("{:?}", entry);
}
// Another example: user-provided filename with fixed extension
let filename = "report[Q1]"; // Literal filename
let escaped = Pattern::escape(filename);
let pattern = format!("/docs/{}.pdf", escaped);
// Matches only the literal file "report[Q1].pdf"
}Mix escaped literals with intentional wildcards for flexible patterns.
Validation Before Escaping
use glob::Pattern;
fn validation_example() {
// Pattern::escape doesn't validate the input
// It just escapes metacharacters
// Invalid path characters pass through:
let null_char = Pattern::escape("file\0.txt");
println!("Null char: {:?}", null_char);
// These may not work as expected on filesystem
// Valid approach: validate path first
fn safe_escape(filename: &str) -> Option<String> {
// Check for null bytes
if filename.contains('\0') {
return None;
}
// Check for other invalid characters based on platform
// Unix: only null is invalid
// Windows: < > : " | ? * and others are invalid
Some(Pattern::escape(filename))
}
}Validate paths before escaping; escape doesn't validate.
Performance Considerations
use glob::Pattern;
fn performance() {
// Pattern::escape is O(n) where n is input length
// It allocates a new String with worst-case 2x size
// For repeated use, compile once:
let filename = "file[2023].txt";
let escaped = Pattern::escape(filename);
let pattern = Pattern::new(&escaped).unwrap();
// pattern can be reused efficiently
assert!(pattern.matches("file[2023].txt"));
assert!(!pattern.matches("file2023.txt"));
// If matching against many filenames, compile once
fn find_matching_files(filenames: &[&str], pattern: &Pattern) -> Vec<&str> {
filenames.iter()
.filter(|f| pattern.matches(f))
.copied()
.collect()
}
}Compile patterns once when matching multiple times.
Pattern Struct Methods
use glob::Pattern;
fn pattern_methods() {
// Pattern::new compiles a pattern for efficient matching
let pattern = Pattern::new("file[*].txt").unwrap();
// Pattern::matches checks if a string matches
assert!(pattern.matches("file*.txt"));
assert!(!pattern.matches("file.txt"));
// Pattern::escape creates a pattern string from literal
let escaped = Pattern::escape("file*.txt");
assert_eq!(escaped, "file[*].txt");
// Combining: escape, then compile
let literal_pattern = Pattern::escape("test[1].txt");
let compiled = Pattern::new(&literal_pattern).unwrap();
assert!(compiled.matches("test[1].txt"));
// AsPath trait for matching paths
use glob::Pattern::matches_path;
let path_pattern = Pattern::new("src/**/*.rs").unwrap();
assert!(path_pattern.matches_path(std::path::Path::new("src/main.rs")));
}Pattern provides both compilation and escape functionality.
Comparison with Shell Escaping
use glob::Pattern;
fn shell_comparison() {
// Shell escaping is different from glob escaping
// Shell escaping uses backslash:
// Shell: file\*.txt
// Matches: literal "file*.txt"
// Glob escaping uses brackets:
// Glob: file[*].txt
// Matches: literal "file*.txt"
// These are NOT equivalent:
let shell_escape = "file\\*.txt"; // Shell-style
let glob_escape = Pattern::escape("file*.txt"); // Glob-style
assert_ne!(shell_escape, glob_escape);
// shell_escape = "file\\*.txt"
// glob_escape = "file[*].txt"
// Glob patterns don't use backslash escaping (except on Windows)
// The bracket approach is specific to glob's character class syntax
}Glob escaping is different from shell escaping.
Complete Example: Safe Filename Search
use glob::{glob, Pattern, GlobError};
use std::path::PathBuf;
fn safe_filename_search() -> Result<Vec<PathBuf>, GlobError> {
// Real-world example: search for user-provided filename pattern
let user_filename = "data[2024].csv"; // Could come from user input
let search_dir = "/var/data";
// Step 1: Escape the filename
let escaped = Pattern::escape(user_filename);
// Step 2: Build the full pattern
let pattern = format!("{}/{}", search_dir, escaped);
// Step 3: Execute the glob
let results: Vec<PathBuf> = glob(&pattern)?
.filter_map(Result::ok)
.collect();
println!("Pattern: {}", pattern);
println!("Found {} files", results.len());
Ok(results)
}
fn pattern_with_wildcard() -> Result<Vec<PathBuf>, GlobError> {
// If you want literal prefix with wildcard suffix:
let prefix = "log[2024]"; // Literal prefix
let escaped_prefix = Pattern::escape(prefix);
// Intentional wildcard for suffix
let pattern = format!("/var/log/{}*.txt", escaped_prefix);
// Matches: /var/log/log[2024]_1.txt, /var/log/log[2024]_2.txt, etc.
Ok(glob(&pattern)?.filter_map(Result::ok).collect())
}Combine escaped literals with intentional wildcards carefully.
Summary Table
use glob::Pattern;
fn summary() {
// | Character | Meaning | Escaped Form |
// |-----------|--------------|--------------|
// | * | Any sequence | [*] |
// | ? | Any single | [?] |
// | [ | Char class | [[] |
// | ] | End class | []] |
// | { | Alternatives | [{] |
// | } | End alt | [}] |
// | other | Literal | unchanged |
// | / | Path sep | unchanged |
// | \ | Escape/sep | unchanged |
}Only glob metacharacters are escaped.
Synthesis
Quick reference:
use glob::Pattern;
// Escape a literal filename for safe glob matching
let filename = "data[2024].csv";
let safe = Pattern::escape(filename);
// safe = "data[[]2024[]].csv"
// Use in glob pattern
let pattern = format!("/var/data/{}", safe);
// pattern = "/var/data/data[[]2024[]].csv"
// Compile and match
let compiled = Pattern::new(&pattern).unwrap();
assert!(compiled.matches("/var/data/data[2024].csv"));
assert!(!compiled.matches("/var/data/data_2024.csv")); // Not a pattern matchWhen to use:
// Always escape when:
// 1. User provides filename or path component
// 2. Filename comes from database or config
// 3. Filename might contain * ? [ ] { }
// 4. Building pattern from variable parts
// Example: Safe pattern builder
fn build_pattern(directory: &str, filename: &str, extension: &str) -> String {
format!(
"{}/{}.{}",
Pattern::escape(directory),
Pattern::escape(filename),
Pattern::escape(extension)
)
}Key insight: Pattern::escape solves a fundamental tension in glob pattern matching: the same characters that enable powerful pattern matching (*, ?, [, ], {, }) can also appear in legitimate filenames. Without escaping, a filename like file[2023].txt would be interpreted as a character class matching file2.txt, file0.txt, file3.txtâcompletely wrong. The escape function leverages glob's character class syntax to neutralize these metacharacters: wrapping * as [*] creates a character class containing only the asterisk character, which matches exactly one asterisk and nothing else. This is elegant because it uses glob's own syntax to escape its metacharactersâno new escape mechanism is needed. The bracket approach [*] works because within a character class, most characters lose their special meaning; only ], -, and a few others remain special. The implementation iterates through the input string, wrapping each metacharacter in brackets, resulting in a pattern that matches the original string exactly. This is essential for security when matching user-provided filenames: without escaping, a malicious user could provide * to match all files, or [a-z] to match unexpected files. Pattern::escape ensures the pattern matches only the intended literal filename, preventing both accidental and intentional pattern expansion.
