blob: 6bd1320fda80b266be88d89bdd0fda5a4ce1bbe4 [file] [log] [blame]
// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
use std::{collections::HashMap, str::FromStr};
use nom::{
branch::alt,
bytes::complete::{is_a, is_not, tag},
character::complete::{alpha1, alphanumeric1},
combinator::{map, recognize},
multi::{many0_count, many1},
sequence::{delimited, pair},
IResult,
};
use once_cell::sync::Lazy;
use regex::Regex;
#[cfg(test)]
use std::path::Path;
#[cfg(test)]
use walkdir::WalkDir;
use crate::{Error, Result};
/// Copy `src` directory to `dest`. `dest` will be created if it or its
/// parents do not exist.
///
/// # Errors
/// Returns errors when:
/// - The `src` directory does not exist.
/// - A directory or file can not be created in the `dest` directory.
/// - The `src` directory contains symlinks.
#[cfg(test)] // Only used in tests. Remove if used elsewhere
pub(crate) fn copy_directory(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> Result<()> {
copy_directory_impl(src.as_ref(), dest.as_ref())
}
#[cfg(test)] // Only used in tests. Remove if used elsewhere
pub(crate) fn copy_directory_impl(src: &Path, dest: &Path) -> Result<()> {
let dest = dest.to_path_buf();
for entry in WalkDir::new(src) {
let entry = entry.map_err(|e| Error::StringErrorPlaceholder(e.to_string()))?;
let src_relative_path = entry
.path()
.strip_prefix(&src)
.expect("child entry should be in directory path");
let dest_location = dest.join(&src_relative_path);
if entry.file_type().is_dir() {
std::fs::create_dir_all(dest_location)?;
} else if entry.file_type().is_file() {
// Because we're walking the directory, a file's parent directory
// will already be created.
std::fs::copy(entry.path(), dest_location)?;
} else if entry.file_type().is_symlink() {
return Err(Error::StringErrorPlaceholder(format!(
"copying symlinks not supported: {entry:?}"
)));
}
}
Ok(())
}
#[derive(Debug)]
enum StringFragment {
Literal(String),
Variable(String),
OpenBrace,
CloseBrace,
}
#[derive(Debug)]
pub struct StringSub {
fragments: Vec<StringFragment>,
}
static VARIABLE_NAME_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_-]*$").expect("regex is valid"));
/// Parser for a `StringSub` variable name. Starts with either a letter or
/// underscore, and can contain alphanumeric characters, underscores, or hyphens.
fn identifier(input: &str) -> IResult<&str, &str> {
recognize(pair(
alt((alpha1, tag("_"))),
many0_count(alt((alphanumeric1, is_a("_-")))),
))(input)
}
/// Parser for a `StringSub` variable placeholder, which is an identifier
/// enclosed in braces.
fn variable_sub(input: &str) -> IResult<&str, StringFragment> {
map(delimited(tag("{"), identifier, tag("}")), |s: &str| {
StringFragment::Variable(s.to_owned())
})(input)
}
/// Parser for a `StringSub` literal string, containing any characters other
/// than braces.
fn string_literal(input: &str) -> IResult<&str, StringFragment> {
map(is_not("{}"), |s: &str| {
StringFragment::Literal(s.to_owned())
})(input)
}
fn escaped_open_brace(input: &str) -> IResult<&str, StringFragment> {
map(tag("{{"), |_| StringFragment::OpenBrace)(input)
}
fn escaped_close_brace(input: &str) -> IResult<&str, StringFragment> {
map(tag("}}"), |_| StringFragment::CloseBrace)(input)
}
impl StringSub {
pub fn valid_variable_name(var: &str) -> bool {
VARIABLE_NAME_REGEX.is_match(var)
}
pub fn new(string: &str) -> Result<Self> {
if string.is_empty() {
return Ok(Self {
fragments: Vec::new(),
});
}
let mut parser = many1(alt((
escaped_open_brace,
escaped_close_brace,
variable_sub,
string_literal,
)));
// TODO(frolv): Map the nom error to a useful user-facing error.
let (remainder, fragments) = parser(string).map_err(|_| Error::GenericErrorPlaceholder)?;
if !remainder.is_empty() {
// TODO(frolv): Some of the string wasn't parsed.
return Err(Error::GenericErrorPlaceholder);
}
Ok(Self { fragments })
}
/// Returns an iterator over the variable names in the string.
pub fn vars(&self) -> impl Iterator<Item = &str> {
self.fragments.iter().filter_map(|f| match f {
StringFragment::Variable(s) => Some(s.as_str()),
_ => None,
})
}
pub fn substitute<'a>(&self, vars: &HashMap<&'a str, &'a str>) -> Result<String> {
self.substitute_cb(|var| vars.get(var).copied())
}
pub fn substitute_cb<'a>(&self, get_var: impl Fn(&str) -> Option<&'a str>) -> Result<String> {
let mut s = String::new();
for frag in &self.fragments {
match frag {
StringFragment::Literal(lit) => s.push_str(lit),
StringFragment::Variable(var) => {
let Some(value) = get_var(var.as_str()) else {
// TODO(frolv): no value provided for `var`.
return Err(Error::GenericErrorPlaceholder);
};
s.push_str(value);
}
StringFragment::OpenBrace => s.push('{'),
StringFragment::CloseBrace => s.push('}'),
}
}
Ok(s)
}
}
impl FromStr for StringSub {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
StringSub::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn substitute_one_var() {
let string = StringSub::new("hello, {foo}!").unwrap();
assert_eq!(string.vars().count(), 1);
let vars = HashMap::from([("foo", "world")]);
assert_eq!(string.substitute(&vars).unwrap(), "hello, world!");
}
#[test]
fn substitute_multiple_vars() {
let string = StringSub::new("{greeting}, {subject}!").unwrap();
assert_eq!(string.vars().count(), 2);
let vars = HashMap::from([("greeting", "hello"), ("subject", "world")]);
assert_eq!(string.substitute(&vars).unwrap(), "hello, world!");
}
#[test]
fn substitute_repeated_var() {
let string = StringSub::new("b{v}{v}a").unwrap();
assert_eq!(string.vars().count(), 2);
let vars = HashMap::from([("v", "an")]);
assert_eq!(string.substitute(&vars).unwrap(), "banana");
}
#[test]
fn substitute_full_string() {
let string = StringSub::new("{foo}").unwrap();
assert_eq!(string.vars().count(), 1);
let vars = HashMap::from([("foo", "hello, world!")]);
assert_eq!(string.substitute(&vars).unwrap(), "hello, world!");
}
#[test]
fn substitute_no_vars() {
let string = StringSub::new("hello").unwrap();
assert_eq!(string.vars().count(), 0);
let mut vars = HashMap::new();
assert_eq!(string.substitute(&vars).unwrap(), "hello");
vars.insert("foo", "world");
assert_eq!(string.substitute(&vars).unwrap(), "hello");
}
#[test]
fn substitute_empty_string() {
let string = StringSub::new("").unwrap();
assert_eq!(string.vars().count(), 0);
let mut vars = HashMap::new();
assert_eq!(string.substitute(&vars).unwrap(), "");
vars.insert("foo", "world");
assert_eq!(string.substitute(&vars).unwrap(), "");
}
#[test]
fn substitute_escaped_braces() {
let string = StringSub::new("hello, {{foo}}").unwrap();
assert_eq!(string.vars().count(), 0);
let mut vars = HashMap::new();
assert_eq!(string.substitute(&vars).unwrap(), "hello, {foo}");
vars.insert("foo", "world");
assert_eq!(string.substitute(&vars).unwrap(), "hello, {foo}");
assert_eq!(
StringSub::new("{{").unwrap().substitute(&vars).unwrap(),
"{",
);
assert_eq!(
StringSub::new("}}").unwrap().substitute(&vars).unwrap(),
"}",
);
assert_eq!(
StringSub::new("{{}}").unwrap().substitute(&vars).unwrap(),
"{}",
);
}
#[test]
fn substitute_variable_names() {
assert!(StringSub::new("{f}").is_ok());
assert!(StringSub::new("{foo}").is_ok());
assert!(StringSub::new("{__foo__}").is_ok());
assert!(StringSub::new("{foo123}").is_ok());
assert!(StringSub::new("{FooBar}").is_ok());
assert!(StringSub::new("{f-o-o-}").is_ok());
assert!(StringSub::new("{123foo}").is_err());
assert!(StringSub::new("{-foo}").is_err());
assert!(StringSub::new("{foo#bar}").is_err());
assert!(StringSub::new("{!%(@)}").is_err());
}
#[test]
fn substitute_invalid_format_string() {
assert!(StringSub::new("{").is_err());
assert!(StringSub::new("}").is_err());
assert!(StringSub::new("hello, {foo").is_err());
assert!(StringSub::new("hello, {}").is_err());
assert!(StringSub::new("this is a closing brace: }").is_err());
assert!(StringSub::new("hello, {foo{").is_err());
assert!(StringSub::new("foo{{bar}baz").is_err());
assert!(StringSub::new("{greeting}, {subject").is_err());
}
#[test]
fn substitute_missing_variables() {
let string = StringSub::new("hello, {foo}!").unwrap();
assert_eq!(string.vars().count(), 1);
let vars = HashMap::new();
assert!(string.substitute(&vars).is_err());
}
}