mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Implement encoding and decoding for Windows-invalid characters in file names
- Added `encode_path_name` and `decode_path_name` functions to handle encoding and decoding of invalid characters. - Updated `SyncbackRules` to include an option for encoding Windows-invalid characters during syncback. - Modified `name_for_inst` to conditionally encode invalid characters based on the new option. - Adjusted `ServeSession` and `InstanceContext` to support the new encoding feature. - Updated tests and snapshots to reflect changes in encoding behavior.
This commit is contained in:
parent
db87fa010f
commit
6d8c85682d
59 changed files with 350 additions and 53 deletions
34
src/json.rs
34
src/json.rs
|
|
@ -166,19 +166,6 @@ pub fn to_vec_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<Vec<u8>>
|
|||
serde_json::to_vec_pretty(&sorted_value).context("Failed to serialize to JSON")
|
||||
}
|
||||
|
||||
/// Serialize a value to a pretty-printed JSON string with sorted keys.
|
||||
///
|
||||
/// This ensures deterministic output by sorting all object keys alphabetically,
|
||||
/// which is especially useful for version control and syncback operations.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the value cannot be serialized to JSON.
|
||||
pub fn to_string_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<String> {
|
||||
let json_value = serde_json::to_value(value).context("Failed to convert to JSON value")?;
|
||||
let sorted_value = sort_json_value(json_value);
|
||||
serde_json::to_string_pretty(&sorted_value).context("Failed to serialize to JSON")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
|
@ -380,25 +367,6 @@ mod tests {
|
|||
assert!(mango_pos < zebra_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_pretty_sorted() {
|
||||
use indexmap::IndexMap;
|
||||
|
||||
let mut map: IndexMap<String, i32> = IndexMap::new();
|
||||
map.insert("zebra".to_string(), 1);
|
||||
map.insert("apple".to_string(), 2);
|
||||
map.insert("mango".to_string(), 3);
|
||||
|
||||
let output = to_string_pretty_sorted(&map).unwrap();
|
||||
|
||||
// Keys should be in alphabetical order
|
||||
let apple_pos = output.find("apple").unwrap();
|
||||
let mango_pos = output.find("mango").unwrap();
|
||||
let zebra_pos = output.find("zebra").unwrap();
|
||||
assert!(apple_pos < mango_pos);
|
||||
assert!(mango_pos < zebra_pos);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sorted_nested_objects() {
|
||||
use serde::Serialize;
|
||||
|
|
@ -413,7 +381,7 @@ mod tests {
|
|||
inner.insert("a".to_string(), 2);
|
||||
|
||||
let nested = Nested { inner };
|
||||
let output = to_string_pretty_sorted(&nested).unwrap();
|
||||
let output = String::from_utf8(to_vec_pretty_sorted(&nested).unwrap()).unwrap();
|
||||
|
||||
// Inner keys should also be sorted
|
||||
let a_pos = output.find("\"a\"").unwrap();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ mod json;
|
|||
mod lua_ast;
|
||||
mod message_queue;
|
||||
mod multimap;
|
||||
pub mod path_encoding;
|
||||
mod path_serializer;
|
||||
mod project;
|
||||
mod resolution;
|
||||
|
|
|
|||
157
src/path_encoding.rs
Normal file
157
src/path_encoding.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! Encoding and decoding utilities for Windows-invalid filename characters.
|
||||
//!
|
||||
//! Windows does not allow the following characters in file names:
|
||||
//! < > : " / \ | ? *
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
/// Mapping of Windows-invalid characters to their encoded representations.
|
||||
const CHAR_ENCODINGS: &[(&str, &str)] = &[
|
||||
("<", "%LT%"),
|
||||
(">", "%GT%"),
|
||||
(":", "%COLON%"),
|
||||
("\"", "%QUOTE%"),
|
||||
("/", "%SLASH%"),
|
||||
("\\", "%BACKSLASH%"),
|
||||
("|", "%PIPE%"),
|
||||
("?", "%QUESTION%"),
|
||||
("*", "%STAR%"),
|
||||
];
|
||||
|
||||
const SPACE_ENCODING: &str = "%SPACE%";
|
||||
|
||||
/// Encodes Windows-invalid 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%`.
|
||||
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();
|
||||
let trailing_spaces = name.len() - name.trim_end_matches(' ').len();
|
||||
|
||||
// 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();
|
||||
for (char, encoded) in CHAR_ENCODINGS {
|
||||
encoded_middle = encoded_middle.replace(char, encoded);
|
||||
}
|
||||
|
||||
// Build final result with encoded spaces
|
||||
let encoded_prefix = SPACE_ENCODING.repeat(leading_spaces);
|
||||
let encoded_suffix = SPACE_ENCODING.repeat(trailing_spaces);
|
||||
|
||||
format!("{}{}{}", encoded_prefix, encoded_middle, encoded_suffix)
|
||||
}
|
||||
|
||||
/// Decodes encoded Windows-invalid 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.
|
||||
pub fn decode_path_name(name: &str) -> String {
|
||||
let mut result = name.to_string();
|
||||
|
||||
// Count and strip leading %SPACE%
|
||||
let mut leading_spaces = 0;
|
||||
while result.starts_with(SPACE_ENCODING) {
|
||||
leading_spaces += 1;
|
||||
result = result[SPACE_ENCODING.len()..].to_string();
|
||||
}
|
||||
|
||||
// Count and strip trailing %SPACE%
|
||||
let mut trailing_spaces = 0;
|
||||
while result.ends_with(SPACE_ENCODING) {
|
||||
trailing_spaces += 1;
|
||||
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);
|
||||
}
|
||||
|
||||
// Rebuild with actual spaces
|
||||
let prefix = " ".repeat(leading_spaces);
|
||||
let suffix = " ".repeat(trailing_spaces);
|
||||
format!("{}{}{}", prefix, result, suffix)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_encode_all_chars() {
|
||||
let input = r#"<>:"/\|?*"#;
|
||||
let expected = "%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#"<>:"/\|?*"#;
|
||||
assert_eq!(decode_path_name(input), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip() {
|
||||
let original = "Test<File>With:Special\"Chars/And\\More|Stuff?Here*End";
|
||||
let encoded = encode_path_name(original);
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_special_chars() {
|
||||
let input = "NormalFileName";
|
||||
assert_eq!(encode_path_name(input), input);
|
||||
assert_eq!(decode_path_name(input), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leading_spaces() {
|
||||
let input = " LeadingSpaces";
|
||||
let encoded = encode_path_name(input);
|
||||
assert_eq!(encoded, "%SPACE%%SPACE%LeadingSpaces");
|
||||
assert_eq!(decode_path_name(&encoded), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_spaces() {
|
||||
let input = "TrailingSpaces ";
|
||||
let encoded = encode_path_name(input);
|
||||
assert_eq!(encoded, "TrailingSpaces%SPACE%%SPACE%");
|
||||
assert_eq!(decode_path_name(&encoded), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_both_spaces() {
|
||||
let input = " Both Spaces ";
|
||||
let encoded = encode_path_name(input);
|
||||
assert_eq!(encoded, "%SPACE%Both Spaces%SPACE%");
|
||||
assert_eq!(decode_path_name(&encoded), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_middle_spaces_unchanged() {
|
||||
let input = "Middle Spaces Here";
|
||||
let encoded = encode_path_name(input);
|
||||
assert_eq!(encoded, "Middle Spaces Here");
|
||||
assert_eq!(decode_path_name(&encoded), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_with_spaces() {
|
||||
let original = " <Test> ";
|
||||
let encoded = encode_path_name(original);
|
||||
let decoded = decode_path_name(&encoded);
|
||||
assert_eq!(decoded, original);
|
||||
}
|
||||
}
|
||||
|
|
@ -106,8 +106,15 @@ impl ServeSession {
|
|||
|
||||
let root_id = tree.get_root_id();
|
||||
|
||||
let instance_context =
|
||||
let decode_windows_invalid_chars = root_project
|
||||
.syncback_rules
|
||||
.as_ref()
|
||||
.map(|rules| rules.encode_windows_invalid_chars())
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut instance_context =
|
||||
InstanceContext::with_emit_legacy_scripts(root_project.emit_legacy_scripts);
|
||||
instance_context.set_decode_windows_invalid_chars(decode_windows_invalid_chars);
|
||||
|
||||
log::trace!("Generating snapshot of instances from VFS");
|
||||
let snapshot = snapshot_from_vfs(&instance_context, &vfs, start_path)?;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,11 @@ pub struct InstanceContext {
|
|||
pub emit_legacy_scripts: bool,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub sync_rules: Vec<SyncRule>,
|
||||
/// Whether to decode Windows-invalid characters in file names.
|
||||
/// Characters like `%LT%`, `%GT%`, `%COLON%`, etc. will be decoded
|
||||
/// back to `<`, `>`, `:`, etc. in instance names sent to the plugin.
|
||||
#[serde(default)]
|
||||
pub decode_windows_invalid_chars: bool,
|
||||
}
|
||||
|
||||
impl InstanceContext {
|
||||
|
|
@ -153,6 +158,7 @@ impl InstanceContext {
|
|||
path_ignore_rules: Arc::new(Vec::new()),
|
||||
emit_legacy_scripts: emit_legacy_scripts_default().unwrap(),
|
||||
sync_rules: Vec::new(),
|
||||
decode_windows_invalid_chars: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +171,10 @@ impl InstanceContext {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_decode_windows_invalid_chars(&mut self, decode: bool) {
|
||||
self.decode_windows_invalid_chars = decode;
|
||||
}
|
||||
|
||||
/// Extend the list of ignore rules in the context with the given new rules.
|
||||
pub fn add_path_ignore_rules<I>(&mut self, new_rules: I)
|
||||
where
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ added_instances:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -172,6 +172,11 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
);
|
||||
continue;
|
||||
}
|
||||
// Skip instances matching ignoreTrees
|
||||
if snapshot.should_ignore_tree(*new_child_ref) {
|
||||
old_child_map.remove(new_child.name.as_str());
|
||||
continue;
|
||||
}
|
||||
if let Some(old_child) = old_child_map.remove(new_child.name.as_str()) {
|
||||
if old_child.metadata().relevant_paths.is_empty() {
|
||||
log::debug!(
|
||||
|
|
@ -223,6 +228,10 @@ pub fn syncback_dir_no_meta<'sync>(
|
|||
);
|
||||
continue;
|
||||
}
|
||||
// Skip instances matching ignoreTrees
|
||||
if snapshot.should_ignore_tree(*new_child_ref) {
|
||||
continue;
|
||||
}
|
||||
children.push(snapshot.with_joined_path(*new_child_ref, None)?);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use crate::{
|
||||
glob::Glob,
|
||||
path_encoding::decode_path_name,
|
||||
project::DEFAULT_PROJECT_NAMES,
|
||||
syncback::{SyncbackReturn, SyncbackSnapshot},
|
||||
};
|
||||
|
|
@ -74,13 +75,19 @@ pub fn snapshot_from_vfs(
|
|||
|
||||
if meta.is_dir() {
|
||||
let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path)?;
|
||||
// Decode directory name if the flag is set
|
||||
let decoded_name = if context.decode_windows_invalid_chars {
|
||||
decode_path_name(dir_name)
|
||||
} else {
|
||||
dir_name.to_string()
|
||||
};
|
||||
// TODO: Support user defined init paths
|
||||
// If and when we do, make sure to go support it in
|
||||
// `Project::set_file_name`, as right now it special-cases
|
||||
// `default.project.json` as an `init` path.
|
||||
match middleware {
|
||||
Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
|
||||
_ => middleware.snapshot(context, vfs, &init_path, dir_name),
|
||||
Middleware::Dir => middleware.snapshot(context, vfs, path, &decoded_name),
|
||||
_ => middleware.snapshot(context, vfs, &init_path, &decoded_name),
|
||||
}
|
||||
} else {
|
||||
let file_name = path
|
||||
|
|
@ -152,19 +159,23 @@ fn snapshot_from_path(
|
|||
vfs: &Vfs,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<Option<InstanceSnapshot>> {
|
||||
// Helper to decode name if the flag is set
|
||||
let decode_name = |name: &str| -> String {
|
||||
if context.decode_windows_invalid_chars {
|
||||
decode_path_name(name)
|
||||
} else {
|
||||
name.to_string()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(rule) = context.get_user_sync_rule(path) {
|
||||
return rule
|
||||
.middleware
|
||||
.snapshot(context, vfs, path, rule.file_name_for_path(path)?);
|
||||
let name = decode_name(rule.file_name_for_path(path)?);
|
||||
return rule.middleware.snapshot(context, vfs, path, &name);
|
||||
} else {
|
||||
for rule in default_sync_rules() {
|
||||
if rule.matches(path) {
|
||||
return rule.middleware.snapshot(
|
||||
context,
|
||||
vfs,
|
||||
path,
|
||||
rule.file_name_for_path(path)?,
|
||||
);
|
||||
let name = decode_name(rule.file_name_for_path(path)?);
|
||||
return rule.middleware.snapshot(context, vfs, path, &name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /root/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /root/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /foo/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /foo/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
@ -45,6 +46,7 @@ children:
|
|||
- /foo/Child/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: dir
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo.model.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
@ -26,6 +27,7 @@ children:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo.model.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
@ -26,6 +27,7 @@ children:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /bar.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /root/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- /root/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /bar.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: false
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ metadata:
|
|||
- /foo/bar.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ metadata:
|
|||
- /foo/bar.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ metadata:
|
|||
- /foo/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ metadata:
|
|||
- /foo/init.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo/default.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo/hello.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ metadata:
|
|||
- /foo/default.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: project
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
@ -31,6 +32,7 @@ children:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ metadata:
|
|||
- /foo/default.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: project
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ metadata:
|
|||
- /foo/default.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: project
|
||||
schema: ~
|
||||
|
|
@ -32,6 +33,7 @@ children:
|
|||
relevant_paths: []
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ metadata:
|
|||
- /foo/default.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: text
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ metadata:
|
|||
- /foo.project.json
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- /foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: ~
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ metadata:
|
|||
- foo.meta.jsonc
|
||||
context:
|
||||
emit_legacy_scripts: true
|
||||
decode_windows_invalid_chars: false
|
||||
specified_id: manually specified
|
||||
middleware: ~
|
||||
schema: ~
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ use std::borrow::Cow;
|
|||
use anyhow::Context;
|
||||
use rbx_dom_weak::Instance;
|
||||
|
||||
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
|
||||
use crate::{
|
||||
path_encoding::encode_path_name, snapshot::InstanceWithMeta, snapshot_middleware::Middleware,
|
||||
};
|
||||
|
||||
pub fn name_for_inst<'old>(
|
||||
middleware: Middleware,
|
||||
new_inst: &Instance,
|
||||
old_inst: Option<InstanceWithMeta<'old>>,
|
||||
encode_invalid_chars: bool,
|
||||
) -> anyhow::Result<Cow<'old, str>> {
|
||||
if let Some(old_inst) = old_inst {
|
||||
if let Some(source) = old_inst.metadata().relevant_paths.first() {
|
||||
|
|
@ -35,13 +38,37 @@ pub fn name_for_inst<'old>(
|
|||
| Middleware::CsvDir
|
||||
| Middleware::ServerScriptDir
|
||||
| Middleware::ClientScriptDir
|
||||
| Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()),
|
||||
| Middleware::ModuleScriptDir => {
|
||||
let name = if encode_invalid_chars {
|
||||
let encoded = encode_path_name(&new_inst.name);
|
||||
// Validate for issues not fixed by encoding (trailing space/dot, control chars, reserved names)
|
||||
validate_file_name(&encoded).with_context(|| {
|
||||
format!("name '{}' (encoded from '{}') is not legal to write to the file system", encoded, new_inst.name)
|
||||
})?;
|
||||
encoded
|
||||
} else {
|
||||
validate_file_name(&new_inst.name).with_context(|| {
|
||||
format!("name '{}' is not legal to write to the file system", new_inst.name)
|
||||
})?;
|
||||
new_inst.name.clone()
|
||||
};
|
||||
Cow::Owned(name)
|
||||
}
|
||||
_ => {
|
||||
let extension = extension_for_middleware(middleware);
|
||||
let name = &new_inst.name;
|
||||
validate_file_name(name).with_context(|| {
|
||||
format!("name '{name}' is not legal to write to the file system")
|
||||
})?;
|
||||
let name = if encode_invalid_chars {
|
||||
let encoded = encode_path_name(&new_inst.name);
|
||||
// Validate for issues not fixed by encoding (trailing space/dot, control chars, reserved names)
|
||||
validate_file_name(&encoded).with_context(|| {
|
||||
format!("name '{}' (encoded from '{}') is not legal to write to the file system", encoded, new_inst.name)
|
||||
})?;
|
||||
encoded
|
||||
} else {
|
||||
validate_file_name(&new_inst.name).with_context(|| {
|
||||
format!("name '{}' is not legal to write to the file system", new_inst.name)
|
||||
})?;
|
||||
new_inst.name.clone()
|
||||
};
|
||||
Cow::Owned(format!("{name}.{extension}"))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -409,6 +409,12 @@ pub struct SyncbackRules {
|
|||
/// generally a better UX.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
create_ignore_dir_paths: Option<bool>,
|
||||
/// Whether to encode Windows-invalid characters in file names during
|
||||
/// syncback. Characters like `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
|
||||
/// will be encoded as `%LT%`, `%GT%`, `%COLON%`, etc.
|
||||
/// Defaults to `false`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
encode_windows_invalid_chars: Option<bool>,
|
||||
}
|
||||
|
||||
impl SyncbackRules {
|
||||
|
|
@ -432,6 +438,13 @@ impl SyncbackRules {
|
|||
|
||||
Ok(globs)
|
||||
}
|
||||
|
||||
/// Returns whether Windows-invalid characters should be encoded in file
|
||||
/// names during syncback. Defaults to `false`.
|
||||
#[inline]
|
||||
pub fn encode_windows_invalid_chars(&self) -> bool {
|
||||
self.encode_windows_invalid_chars.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_path(globs: &Option<Vec<Glob>>, base_path: &Path, path: &Path) -> bool {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,13 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
middleware: None,
|
||||
};
|
||||
let middleware = get_best_middleware(&snapshot);
|
||||
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
||||
let encode_invalid_chars = self.encode_windows_invalid_chars();
|
||||
let name = name_for_inst(
|
||||
middleware,
|
||||
snapshot.new_inst(),
|
||||
snapshot.old_inst(),
|
||||
encode_invalid_chars,
|
||||
)?;
|
||||
snapshot.path = self.path.join(name.as_ref());
|
||||
|
||||
Ok(snapshot)
|
||||
|
|
@ -70,7 +76,13 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
middleware: None,
|
||||
};
|
||||
let middleware = get_best_middleware(&snapshot);
|
||||
let name = name_for_inst(middleware, snapshot.new_inst(), snapshot.old_inst())?;
|
||||
let encode_invalid_chars = self.encode_windows_invalid_chars();
|
||||
let name = name_for_inst(
|
||||
middleware,
|
||||
snapshot.new_inst(),
|
||||
snapshot.old_inst(),
|
||||
encode_invalid_chars,
|
||||
)?;
|
||||
snapshot.path = base_path.join(name.as_ref());
|
||||
|
||||
Ok(snapshot)
|
||||
|
|
@ -224,6 +236,34 @@ impl<'sync> SyncbackSnapshot<'sync> {
|
|||
.map(|classes| classes.iter().any(|c| c == class_name))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Checks if an instance should be ignored based on ignoreTrees rules.
|
||||
/// Takes a Ref to check against the new tree's instance path.
|
||||
#[inline]
|
||||
pub fn should_ignore_tree(&self, referent: Ref) -> bool {
|
||||
if let Some(ignore_trees) = self.ignore_tree() {
|
||||
let path = inst_path(self.new_tree(), referent);
|
||||
for ignored in ignore_trees {
|
||||
if path.starts_with(ignored.as_str()) {
|
||||
log::debug!("Ignoring {path} because it matches ignoreTrees entry: {ignored}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns whether Windows-invalid characters should be encoded in file
|
||||
/// names during syncback.
|
||||
#[inline]
|
||||
pub fn encode_windows_invalid_chars(&self) -> bool {
|
||||
self.data
|
||||
.project
|
||||
.syncback_rules
|
||||
.as_ref()
|
||||
.map(|rules| rules.encode_windows_invalid_chars())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_out_property(inst: &Instance, prop_name: &str) -> bool {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue