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:
Joe 2025-12-19 22:50:53 -06:00
parent db87fa010f
commit 6d8c85682d
59 changed files with 350 additions and 53 deletions

View file

@ -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();

View file

@ -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
View 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);
}
}

View file

@ -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)?;

View file

@ -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

View file

@ -13,6 +13,7 @@ metadata:
relevant_paths: []
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -11,6 +11,7 @@ metadata:
relevant_paths: []
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
relevant_paths: []
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -11,6 +11,7 @@ metadata:
relevant_paths: []
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -12,6 +12,7 @@ added_instances:
relevant_paths: []
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -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)?);
}
}

View file

@ -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);
}
}
}

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -20,6 +20,7 @@ metadata:
- /root/init.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -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: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -20,6 +20,7 @@ metadata:
- /foo/init.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -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: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: manually specified
middleware: ~
schema: ~

View file

@ -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: ~

View file

@ -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: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /bar.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -20,6 +20,7 @@ metadata:
- /root/init.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -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: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /bar.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: false
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -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: ~

View file

@ -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: ~

View file

@ -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: ~

View file

@ -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: ~

View file

@ -11,6 +11,7 @@ metadata:
- /foo/default.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -11,6 +11,7 @@ metadata:
- /foo/hello.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -12,6 +12,7 @@ metadata:
- /foo/default.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: project
schema: ~

View file

@ -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: ~

View file

@ -12,6 +12,7 @@ metadata:
- /foo/default.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: project
schema: ~

View file

@ -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: ~

View file

@ -14,6 +14,7 @@ metadata:
- /foo/default.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: text
schema: ~

View file

@ -11,6 +11,7 @@ metadata:
- /foo.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -11,6 +11,7 @@ metadata:
- /foo.project.json
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: manually specified
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: manually specified
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- /foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: ~
middleware: ~
schema: ~

View file

@ -13,6 +13,7 @@ metadata:
- foo.meta.jsonc
context:
emit_legacy_scripts: true
decode_windows_invalid_chars: false
specified_id: manually specified
middleware: ~
schema: ~

View file

@ -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}"))
}
})

View file

@ -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 {

View file

@ -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 {