mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
RunContext refactor + sync updates
This commit is contained in:
parent
c7cddb72c2
commit
d3cc11659a
79 changed files with 1415 additions and 626 deletions
|
|
@ -1,15 +1,20 @@
|
|||
//! Encoding and decoding utilities for Windows-invalid filename characters.
|
||||
//! Encoding and decoding utilities for special filename characters.
|
||||
//!
|
||||
//! Windows does not allow the following characters in file names:
|
||||
//! < > : " / \ | ? *
|
||||
//! This module encodes characters that are problematic in file names:
|
||||
//! - Windows-invalid characters: < > : " / \ | ? *
|
||||
//! - Periods (.) to prevent conflicts with file extension parsing
|
||||
//! - Leading/trailing spaces
|
||||
//! - Literal percent signs (%) to allow round-tripping
|
||||
//!
|
||||
//! Windows also does not allow leading/trailing spaces in file names.
|
||||
//!
|
||||
//! This module provides functions to encode these characters using a %NAME%
|
||||
//! format (e.g., `<` becomes `%LT%`) and decode them back.
|
||||
//! Characters are encoded using a %NAME% format (e.g., `<` becomes `%LT%`,
|
||||
//! `.` becomes `%DOT%`). Literal `%` is escaped as `%%` (like printf).
|
||||
|
||||
/// Mapping of Windows-invalid characters to their encoded representations.
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Mapping of special characters to their encoded representations.
|
||||
const CHAR_ENCODINGS: &[(&str, &str)] = &[
|
||||
(".", "%DOT%"),
|
||||
("<", "%LT%"),
|
||||
(">", "%GT%"),
|
||||
(":", "%COLON%"),
|
||||
|
|
@ -21,13 +26,27 @@ const CHAR_ENCODINGS: &[(&str, &str)] = &[
|
|||
("*", "%STAR%"),
|
||||
];
|
||||
|
||||
/// Returns a map from encoded pattern (e.g., "DOT") to decoded char (e.g., ".").
|
||||
fn get_decode_map() -> &'static HashMap<&'static str, &'static str> {
|
||||
static DECODE_MAP: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
|
||||
DECODE_MAP.get_or_init(|| {
|
||||
let mut map = HashMap::new();
|
||||
for (char, encoded) in CHAR_ENCODINGS {
|
||||
// Strip the leading and trailing % from the pattern
|
||||
let pattern = &encoded[1..encoded.len() - 1];
|
||||
map.insert(pattern, *char);
|
||||
}
|
||||
map
|
||||
})
|
||||
}
|
||||
|
||||
const SPACE_ENCODING: &str = "%SPACE%";
|
||||
|
||||
/// Encodes Windows-invalid characters in a file name.
|
||||
/// Encodes special characters in a file name.
|
||||
///
|
||||
/// This replaces characters like `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
|
||||
/// with their encoded representations like `%LT%`, `%GT%`, etc.
|
||||
/// Leading and trailing spaces are encoded as `%SPACE%`.
|
||||
/// This replaces characters like `.`, `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
|
||||
/// with their encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc.
|
||||
/// Literal `%` is escaped as `%%`. Leading and trailing spaces are encoded as `%SPACE%`.
|
||||
pub fn encode_path_name(name: &str) -> String {
|
||||
// Count leading and trailing spaces first (before any modifications)
|
||||
let leading_spaces = name.len() - name.trim_start_matches(' ').len();
|
||||
|
|
@ -36,8 +55,10 @@ pub fn encode_path_name(name: &str) -> String {
|
|||
// Get the middle part (without leading/trailing spaces)
|
||||
let middle = &name[leading_spaces..name.len() - trailing_spaces];
|
||||
|
||||
// Encode invalid characters in the middle part
|
||||
let mut encoded_middle = middle.to_string();
|
||||
// First: escape % as %% (before adding new % signs from encodings)
|
||||
let mut encoded_middle = middle.replace('%', "%%");
|
||||
|
||||
// Then: encode special characters
|
||||
for (char, encoded) in CHAR_ENCODINGS {
|
||||
encoded_middle = encoded_middle.replace(char, encoded);
|
||||
}
|
||||
|
|
@ -49,11 +70,13 @@ pub fn encode_path_name(name: &str) -> String {
|
|||
format!("{}{}{}", encoded_prefix, encoded_middle, encoded_suffix)
|
||||
}
|
||||
|
||||
/// Decodes encoded Windows-invalid characters back to their original form.
|
||||
/// Decodes encoded special characters back to their original form.
|
||||
///
|
||||
/// This replaces encoded representations like `%LT%`, `%GT%`, etc. back
|
||||
/// to their original characters `<`, `>`, etc.
|
||||
/// `%SPACE%` at the start/end is decoded back to spaces.
|
||||
/// This replaces encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc.
|
||||
/// back to their original characters `.`, `<`, `>`, etc.
|
||||
/// `%%` is decoded to literal `%`. `%SPACE%` at the start/end is decoded back to spaces.
|
||||
///
|
||||
/// Uses a proper left-to-right parser to correctly handle `%%` escaping.
|
||||
pub fn decode_path_name(name: &str) -> String {
|
||||
let mut result = name.to_string();
|
||||
|
||||
|
|
@ -71,15 +94,83 @@ pub fn decode_path_name(name: &str) -> String {
|
|||
result = result[..result.len() - SPACE_ENCODING.len()].to_string();
|
||||
}
|
||||
|
||||
// Decode invalid characters in the middle
|
||||
for (char, encoded) in CHAR_ENCODINGS {
|
||||
result = result.replace(encoded, char);
|
||||
}
|
||||
// Parse and decode the middle part
|
||||
let decoded_middle = decode_patterns(&result);
|
||||
|
||||
// Rebuild with actual spaces
|
||||
let prefix = " ".repeat(leading_spaces);
|
||||
let suffix = " ".repeat(trailing_spaces);
|
||||
format!("{}{}{}", prefix, result, suffix)
|
||||
format!("{}{}{}", prefix, decoded_middle, suffix)
|
||||
}
|
||||
|
||||
/// Parses an encoded string left-to-right, properly handling `%%` escapes and `%NAME%` patterns.
|
||||
fn decode_patterns(input: &str) -> String {
|
||||
let decode_map = get_decode_map();
|
||||
let chars: Vec<char> = input.chars().collect();
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
if chars[i] == '%' {
|
||||
// Check for %% (escaped percent)
|
||||
if i + 1 < chars.len() && chars[i + 1] == '%' {
|
||||
output.push('%');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to match a %NAME% pattern
|
||||
if let Some((decoded, consumed)) = try_decode_pattern(&chars, i, decode_map) {
|
||||
output.push_str(&decoded);
|
||||
i += consumed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular character, just copy it
|
||||
output.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Tries to decode a %NAME% pattern starting at position `start`.
|
||||
/// Returns Some((decoded_string, chars_consumed)) if successful, None otherwise.
|
||||
fn try_decode_pattern(
|
||||
chars: &[char],
|
||||
start: usize,
|
||||
decode_map: &HashMap<&str, &str>,
|
||||
) -> Option<(String, usize)> {
|
||||
// Must start with %
|
||||
if chars.get(start) != Some(&'%') {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the closing %
|
||||
let mut end = start + 1;
|
||||
while end < chars.len() && chars[end] != '%' {
|
||||
// Pattern names are uppercase letters only
|
||||
if !chars[end].is_ascii_uppercase() {
|
||||
return None;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
|
||||
// Must have found a closing %
|
||||
if end >= chars.len() || chars[end] != '%' {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Extract the pattern name (without the % delimiters)
|
||||
let pattern_name: String = chars[start + 1..end].iter().collect();
|
||||
|
||||
// Look up the pattern
|
||||
if let Some(&decoded) = decode_map.get(pattern_name.as_str()) {
|
||||
Some((decoded.to_string(), end - start + 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -88,15 +179,15 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_encode_all_chars() {
|
||||
let input = r#"<>:"/\|?*"#;
|
||||
let expected = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||
let input = r#".<>:"/\|?*"#;
|
||||
let expected = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||
assert_eq!(encode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_all_chars() {
|
||||
let input = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||
let expected = r#"<>:"/\|?*"#;
|
||||
let input = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||
let expected = r#".<>:"/\|?*"#;
|
||||
assert_eq!(decode_path_name(input), expected);
|
||||
}
|
||||
|
||||
|
|
@ -154,4 +245,67 @@ mod tests {
|
|||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_period() {
|
||||
let input = "My.Script";
|
||||
let expected = "My%DOT%Script";
|
||||
assert_eq!(encode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_period() {
|
||||
let input = "My%DOT%Script";
|
||||
let expected = "My.Script";
|
||||
assert_eq!(decode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_with_period() {
|
||||
let original = "My.Module.Name";
|
||||
let encoded = encode_path_name(original);
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_percent() {
|
||||
let input = "My%Thing";
|
||||
let expected = "My%%Thing";
|
||||
assert_eq!(encode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_percent() {
|
||||
let input = "My%%Thing";
|
||||
let expected = "My%Thing";
|
||||
assert_eq!(decode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_with_percent() {
|
||||
let original = "My%Thing";
|
||||
let encoded = encode_path_name(original);
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_encoding_pattern_roundtrip() {
|
||||
// This tests that a name containing what looks like an encoding pattern
|
||||
// round-trips correctly and doesn't get incorrectly decoded
|
||||
let original = "My%DOT%Thing";
|
||||
let encoded = encode_path_name(original);
|
||||
assert_eq!(encoded, "My%%DOT%%Thing");
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combined_period_and_percent() {
|
||||
let original = "My.%Thing.Other";
|
||||
let encoded = encode_path_name(original);
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue