//! 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>, ) -> anyhow::Result> { 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::PluginScriptDir | 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::PluginScriptDir | 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>(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(()) }