forked from rojo-rbx/rojo
172 lines
5.8 KiB
Rust
172 lines
5.8 KiB
Rust
//! 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<'a>(
|
|
middleware: Middleware,
|
|
new_inst: &'a Instance,
|
|
old_inst: Option<InstanceWithMeta<'a>>,
|
|
) -> anyhow::Result<Cow<'a, 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 => {
|
|
if validate_file_name(&new_inst.name).is_err() {
|
|
Cow::Owned(slugify_name(&new_inst.name))
|
|
} else {
|
|
Cow::Borrowed(&new_inst.name)
|
|
}
|
|
}
|
|
_ => {
|
|
let extension = extension_for_middleware(middleware);
|
|
let slugified;
|
|
let final_name = if validate_file_name(&new_inst.name).is_err() {
|
|
slugified = slugify_name(&new_inst.name);
|
|
&slugified
|
|
} else {
|
|
&new_inst.name
|
|
};
|
|
|
|
Cow::Owned(format!("{final_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] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
|
|
|
/// Slugifies a name by replacing forbidden characters with underscores
|
|
/// and ensuring the result is a valid file name
|
|
pub fn slugify_name(name: &str) -> String {
|
|
let mut result = String::with_capacity(name.len());
|
|
|
|
for ch in name.chars() {
|
|
if FORBIDDEN_CHARS.contains(&ch) {
|
|
result.push('_');
|
|
} else {
|
|
result.push(ch);
|
|
}
|
|
}
|
|
|
|
// Handle Windows reserved names by appending an underscore
|
|
let result_lower = result.to_lowercase();
|
|
for forbidden in INVALID_WINDOWS_NAMES {
|
|
if result_lower == forbidden.to_lowercase() {
|
|
result.push('_');
|
|
break;
|
|
}
|
|
}
|
|
|
|
while result.ends_with(' ') || result.ends_with('.') {
|
|
result.pop();
|
|
}
|
|
|
|
if result.is_empty() || result.chars().all(|c| c == '_') {
|
|
result = "instance".to_string();
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// 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(())
|
|
}
|