Implement Syncback to support converting Roblox files to a Rojo project (#937)

This is a very large commit.
Consider checking the linked PR for more information.
This commit is contained in:
Micah
2025-11-19 09:21:33 -08:00
committed by GitHub
parent 071b6e7e23
commit 9b5a07191b
239 changed files with 5325 additions and 225 deletions

128
src/syncback/file_names.rs Normal file
View File

@@ -0,0 +1,128 @@
//! Contains logic for generating new file names for Instances based on their
//! middleware.
use std::borrow::Cow;
use anyhow::Context;
use rbx_dom_weak::Instance;
use crate::{snapshot::InstanceWithMeta, snapshot_middleware::Middleware};
pub fn name_for_inst<'old>(
middleware: Middleware,
new_inst: &Instance,
old_inst: Option<InstanceWithMeta<'old>>,
) -> anyhow::Result<Cow<'old, str>> {
if let Some(old_inst) = old_inst {
if let Some(source) = old_inst.metadata().relevant_paths.first() {
source
.file_name()
.and_then(|s| s.to_str())
.map(Cow::Borrowed)
.context("sources on the file system should be valid unicode and not be stubs")
} else {
// This is technically not /always/ true, but we want to avoid
// running syncback on anything that has no instigating source
// anyway.
anyhow::bail!(
"members of 'old' trees should have an instigating source. Somehow, {} did not.",
old_inst.name(),
);
}
} else {
Ok(match middleware {
Middleware::Dir
| Middleware::CsvDir
| Middleware::ServerScriptDir
| Middleware::ClientScriptDir
| Middleware::ModuleScriptDir => Cow::Owned(new_inst.name.clone()),
_ => {
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")
})?;
Cow::Owned(format!("{name}.{extension}"))
}
})
}
}
/// Returns the extension a provided piece of middleware is supposed to use.
pub fn extension_for_middleware(middleware: Middleware) -> &'static str {
match middleware {
Middleware::Csv => "csv",
Middleware::JsonModel => "model.json",
Middleware::Json => "json",
Middleware::ServerScript => "server.luau",
Middleware::ClientScript => "client.luau",
Middleware::ModuleScript => "luau",
Middleware::PluginScript => "plugin.luau",
Middleware::Project => "project.json",
Middleware::Rbxm => "rbxm",
Middleware::Rbxmx => "rbxmx",
Middleware::Toml => "toml",
Middleware::Text => "txt",
Middleware::Yaml => "yml",
Middleware::LegacyServerScript
| Middleware::LegacyClientScript
| Middleware::RunContextServerScript
| Middleware::RunContextClientScript => {
todo!("syncback does not work on the middleware {middleware:?} yet")
}
// These are manually specified and not `_` to guard against future
// middleware additions missing this function.
Middleware::Ignore => unimplemented!("syncback does not work on Ignore middleware"),
Middleware::Dir
| Middleware::CsvDir
| Middleware::ServerScriptDir
| Middleware::ClientScriptDir
| Middleware::ModuleScriptDir => {
unimplemented!("directory middleware requires special treatment")
}
}
}
/// A list of file names that are not valid on Windows.
const INVALID_WINDOWS_NAMES: [&str; 22] = [
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
];
/// A list of all characters that are outright forbidden to be included
/// in a file's name.
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
/// Validates a provided file name to ensure it's allowed on the file system. An
/// error is returned if the name isn't allowed, indicating why.
/// This takes into account rules for Windows, MacOS, and Linux.
///
/// In practice however, these broadly overlap so the only unexpected behavior
/// is Windows, where there are 22 reserved names.
pub fn validate_file_name<S: AsRef<str>>(name: S) -> anyhow::Result<()> {
let str = name.as_ref();
if str.ends_with(' ') {
anyhow::bail!("file names cannot end with a space")
}
if str.ends_with('.') {
anyhow::bail!("file names cannot end with '.'")
}
for char in str.chars() {
if FORBIDDEN_CHARS.contains(&char) {
anyhow::bail!("file names cannot contain <, >, :, \", /, |, ?, *, or \\")
} else if char.is_control() {
anyhow::bail!("file names cannot contain control characters")
}
}
for forbidden in INVALID_WINDOWS_NAMES {
if str == forbidden {
anyhow::bail!("files cannot be named {str}")
}
}
Ok(())
}