diff --git a/CHANGELOG.md b/CHANGELOG.md index 3156635..981a26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.11.0 + +- `fs` option added to allow interception and reimplementation of all file system operations (such as imports) +- `wasm` feature renamed to/replaced with `wasm-exports`, which no longer materially alters the API: `from_path` is reinstated, and `from_string` once again returns the full error type; but the WASM export `from_string` (which returns a string error) is now a new function `from_string_js`. (It was renamed from `wasm` to `wasm-exports` because the name was misleading; Rust code that uses grass doesn’t need this feature, it’s solely to get this `from_string` WASM export.) + # 0.10.8 - bugfix: properly emit the number `0` in compressed mode (#53) diff --git a/Cargo.toml b/Cargo.toml index 41b346b..e6e65c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,8 +74,8 @@ commandline = ["clap"] nightly = [] # Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()` random = ["rand"] -# Option: compile to web assembly -wasm = ["wasm-bindgen"] +# Option: expose JavaScript-friendly WebAssembly exports +wasm-exports = ["wasm-bindgen"] # Option: enable features that assist in profiling (e.g. inline(never)) profiling = [] # Option: enable criterion for benchmarking diff --git a/src/fs.rs b/src/fs.rs new file mode 100644 index 0000000..24d8ad5 --- /dev/null +++ b/src/fs.rs @@ -0,0 +1,70 @@ +use std::io::{Error, ErrorKind, Result}; +use std::path::Path; + +/// A trait to allow replacing the file system lookup mechanisms. +/// +/// As it stands, this is imperfect: it’s still using the types and some operations from +/// `std::path`, which constrain it to the target platform’s norms. This could be ameliorated by +/// the use of associated types for `Path` and `PathBuf`, and putting all remaining methods on this +/// trait (`is_absolute`, `parent`, `join`, *&c.*); but that would infect too many other APIs to be +/// desirable, so we live with it as it is—which is also acceptable, because the motivating example +/// use case is mostly using this as an optimisation over the real platform underneath. +pub trait Fs: std::fmt::Debug { + /// Returns `true` if the path exists on disk and is pointing at a directory. + fn is_dir(&self, path: &Path) -> bool; + /// Returns `true` if the path exists on disk and is pointing at a regular file. + fn is_file(&self, path: &Path) -> bool; + /// Read the entire contents of a file into a bytes vector. + fn read(&self, path: &Path) -> Result>; +} + +/// Use [`std::fs`] to read any files from disk. +/// +/// This is the default file system implementation. +#[derive(Debug)] +pub struct StdFs; + +impl Fs for StdFs { + #[inline] + fn is_file(&self, path: &Path) -> bool { + path.is_file() + } + + #[inline] + fn is_dir(&self, path: &Path) -> bool { + path.is_dir() + } + + #[inline] + fn read(&self, path: &Path) -> Result> { + std::fs::read(path) + } +} + +/// A file system implementation that acts like it’s completely empty. +/// +/// This may be useful for security as it denies all access to the file system (so `@import` is +/// prevented from leaking anything); you’ll need to use [`from_string`][crate::from_string] for +/// this to make any sense (since [`from_path`][crate::from_path] would fail to find a file). +#[derive(Debug)] +pub struct NullFs; + +impl Fs for NullFs { + #[inline] + fn is_file(&self, _path: &Path) -> bool { + false + } + + #[inline] + fn is_dir(&self, _path: &Path) -> bool { + false + } + + #[inline] + fn read(&self, _path: &Path) -> Result> { + Err(Error::new( + ErrorKind::NotFound, + "NullFs, there is no file system", + )) + } +} diff --git a/src/lib.rs b/src/lib.rs index a9d0414..c545b06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,9 +92,10 @@ grass input.scss )] #![cfg_attr(feature = "nightly", feature(track_caller))] #![cfg_attr(feature = "profiling", inline(never))] -use std::{fs, path::Path}; -#[cfg(feature = "wasm")] +use std::path::Path; + +#[cfg(feature = "wasm-exports")] use wasm_bindgen::prelude::*; pub(crate) use beef::lean::Cow; @@ -102,6 +103,7 @@ pub(crate) use beef::lean::Cow; use codemap::CodeMap; pub use crate::error::{SassError as Error, SassResult as Result}; +pub use crate::fs::{Fs, NullFs, StdFs}; pub(crate) use crate::token::Token; use crate::{ builtin::modules::{ModuleConfig, Modules}, @@ -121,6 +123,7 @@ mod builtin; mod color; mod common; mod error; +mod fs; mod interner; mod lexer; mod output; @@ -152,6 +155,7 @@ pub enum OutputStyle { /// more control. #[derive(Debug)] pub struct Options<'a> { + fs: &'a dyn Fs, style: OutputStyle, load_paths: Vec<&'a Path>, allows_charset: bool, @@ -163,6 +167,7 @@ impl Default for Options<'_> { #[inline] fn default() -> Self { Self { + fs: &StdFs, style: OutputStyle::Expanded, load_paths: Vec::new(), allows_charset: true, @@ -173,6 +178,17 @@ impl Default for Options<'_> { } impl<'a> Options<'a> { + /// This option allows you to control the file system that Sass will see. + /// + /// By default, it uses [`StdFs`], which is backed by [`std::fs`], + /// allowing direct, unfettered access to the local file system. + #[must_use] + #[inline] + pub fn fs(mut self, fs: &'a dyn Fs) -> Self { + self.fs = fs; + self + } + /// `grass` currently offers 2 different output styles /// /// - `OutputStyle::Expanded` writes each selector and declaration on its own line. @@ -317,9 +333,8 @@ fn from_string_with_file_name(input: String, file_name: &str, options: &Options) /// ``` #[cfg_attr(feature = "profiling", inline(never))] #[cfg_attr(not(feature = "profiling"), inline)] -#[cfg(not(feature = "wasm"))] pub fn from_path(p: &str, options: &Options) -> Result { - from_string_with_file_name(String::from_utf8(fs::read(p)?)?, p, options) + from_string_with_file_name(String::from_utf8(options.fs.read(Path::new(p))?)?, p, options) } /// Compile CSS from a string @@ -333,41 +348,12 @@ pub fn from_path(p: &str, options: &Options) -> Result { /// ``` #[cfg_attr(feature = "profiling", inline(never))] #[cfg_attr(not(feature = "profiling"), inline)] -#[cfg(not(feature = "wasm"))] pub fn from_string(input: String, options: &Options) -> Result { from_string_with_file_name(input, "stdin", options) } -#[cfg(feature = "wasm")] -#[wasm_bindgen] -pub fn from_string(p: String) -> std::result::Result { - let mut map = CodeMap::new(); - let file = map.add_file("stdin".into(), p); - let empty_span = file.span.subspan(0, 0); - - let stmts = Parser { - toks: &mut Lexer::new_from_file(&file), - map: &mut map, - path: Path::new(""), - scopes: &mut Scopes::new(), - global_scope: &mut Scope::new(), - super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)), - span_before: empty_span, - content: &mut Vec::new(), - flags: ContextFlags::empty(), - at_root: true, - at_root_has_selector: false, - extender: &mut Extender::new(empty_span), - content_scopes: &mut Scopes::new(), - options: &Options::default(), - modules: &mut Modules::default(), - module_config: &mut ModuleConfig::default(), - } - .parse() - .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?; - - Ok(Css::from_stmts(stmts, false, true) - .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())? - .pretty_print(&map, options.style) - .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?) +#[cfg(feature = "wasm-exports")] +#[wasm_bindgen(js_name = from_string)] +pub fn from_string_js(p: String) -> std::result::Result { + from_string(Options::default()).map_err(|e| e.to_string()) } diff --git a/src/main.rs b/src/main.rs index ca933cc..8a1de37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,6 @@ use std::{ use clap::{arg_enum, App, AppSettings, Arg}; -#[cfg(not(feature = "wasm"))] use grass::{from_path, from_string, Options, OutputStyle}; // TODO remove this @@ -26,10 +25,6 @@ arg_enum! { } } -#[cfg(feature = "wasm")] -fn main() {} - -#[cfg(not(feature = "wasm"))] #[cfg_attr(feature = "profiling", inline(never))] fn main() -> std::io::Result<()> { let matches = App::new("grass") diff --git a/src/parse/control_flow.rs b/src/parse/control_flow.rs index 54f3d84..4b374d7 100644 --- a/src/parse/control_flow.rs +++ b/src/parse/control_flow.rs @@ -13,6 +13,48 @@ use crate::{ }; impl<'a, 'b> Parser<'a, 'b> { + fn subparser_with_in_control_flow_flag<'c>(&'c mut self) -> Parser<'c, 'b> { + Parser { + toks: self.toks, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content, + flags: self.flags | ContextFlags::IN_CONTROL_FLOW, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + content_scopes: self.content_scopes, + options: self.options, + modules: self.modules, + module_config: self.module_config, + } + } + + fn with_toks<'d>(self, toks: &'a mut Lexer<'d>) -> Parser<'a, 'd> { + Parser { + toks, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content, + flags: self.flags, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + content_scopes: self.content_scopes, + options: self.options, + modules: self.modules, + module_config: self.module_config, + } + } + pub(super) fn parse_if(&mut self) -> SassResult> { self.whitespace_or_comment(); @@ -28,29 +70,9 @@ impl<'a, 'b> Parser<'a, 'b> { } if init_cond.is_true() { - self.scopes.enter_new_scope(); found_true = true; - - body = Parser { - toks: self.toks, - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt()?; - + self.scopes.enter_new_scope(); + body = self.subparser_with_in_control_flow_flag().parse_stmt()?; self.scopes.exit_scope(); } else { self.throw_away_until_closing_curly_brace()?; @@ -91,26 +113,7 @@ impl<'a, 'b> Parser<'a, 'b> { if cond { found_true = true; self.scopes.enter_new_scope(); - body = Parser { - toks: self.toks, - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt()?; - + body = self.subparser_with_in_control_flow_flag().parse_stmt()?; self.scopes.exit_scope(); } else { self.throw_away_until_closing_curly_brace()?; @@ -125,25 +128,7 @@ impl<'a, 'b> Parser<'a, 'b> { } self.scopes.enter_new_scope(); - let tmp = Parser { - toks: self.toks, - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt(); + let tmp = self.subparser_with_in_control_flow_flag().parse_stmt(); self.scopes.exit_scope(); return tmp; } @@ -272,51 +257,15 @@ impl<'a, 'b> Parser<'a, 'b> { var.node, Value::Dimension(Some(Number::from(i)), Unit::None, true), ); - if self.flags.in_function() { - let these_stmts = Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } + let mut these_stmts = self.subparser_with_in_control_flow_flag() + .with_toks(&mut Lexer::new_ref(&body)) .parse_stmt()?; + if self.flags.in_function() { if !these_stmts.is_empty() { return Ok(these_stmts); } } else { - stmts.append( - &mut Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt()?, - ); + stmts.append(&mut these_stmts); } } @@ -349,51 +298,15 @@ impl<'a, 'b> Parser<'a, 'b> { let mut val = self.parse_value_from_vec(&cond, true)?; self.scopes.enter_new_scope(); while val.node.is_true() { - if self.flags.in_function() { - let these_stmts = Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } + let mut these_stmts = self.subparser_with_in_control_flow_flag() + .with_toks(&mut Lexer::new_ref(&body)) .parse_stmt()?; + if self.flags.in_function() { if !these_stmts.is_empty() { return Ok(these_stmts); } } else { - stmts.append( - &mut Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt()?, - ); + stmts.append(&mut these_stmts); } val = self.parse_value_from_vec(&cond, true)?; } @@ -461,51 +374,15 @@ impl<'a, 'b> Parser<'a, 'b> { } } - if self.flags.in_function() { - let these_stmts = Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } + let mut these_stmts = self.subparser_with_in_control_flow_flag() + .with_toks(&mut Lexer::new_ref(&body)) .parse_stmt()?; + if self.flags.in_function() { if !these_stmts.is_empty() { return Ok(these_stmts); } } else { - stmts.append( - &mut Parser { - toks: &mut Lexer::new_ref(&body), - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content, - flags: self.flags | ContextFlags::IN_CONTROL_FLOW, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - extender: self.extender, - content_scopes: self.content_scopes, - options: self.options, - modules: self.modules, - module_config: self.module_config, - } - .parse_stmt()?, - ); + stmts.append(&mut these_stmts); } } diff --git a/src/parse/import.rs b/src/parse/import.rs index e037e7d..ebc0b5b 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -1,4 +1,4 @@ -use std::{ffi::OsStr, fs, path::Path, path::PathBuf}; +use std::{ffi::OsStr, path::Path, path::PathBuf}; use codemap::{Span, Spanned}; @@ -45,49 +45,41 @@ impl<'a, 'b> Parser<'a, 'b> { let name = path_buf.file_name().unwrap_or_else(|| OsStr::new("..")); - let paths = [ - path_buf.with_file_name(name).with_extension("scss"), - path_buf - .with_file_name(format!("_{}", name.to_str().unwrap())) - .with_extension("scss"), - path_buf.clone(), - path_buf.join("index.scss"), - path_buf.join("_index.scss"), - ]; - - for name in &paths { - if name.is_file() { - return Some(name.clone()); - } - } - - for path in &self.options.load_paths { - let paths: Vec = if path.is_dir() { - vec![ - path.join(&path_buf) - .with_file_name(name) - .with_extension("scss"), - path.join(&path_buf) - .with_file_name(format!("_{}", name.to_str().unwrap())) - .with_extension("scss"), - path.join(&path_buf).join("index.scss"), - path.join(&path_buf).join("_index.scss"), - ] - } else { - vec![ - path.to_path_buf(), - path.with_file_name(name).with_extension("scss"), - path.with_file_name(format!("_{}", name.to_str().unwrap())) - .with_extension("scss"), - path.join("index.scss"), - path.join("_index.scss"), - ] - }; - - for name in paths { - if name.is_file() { + macro_rules! try_path { + ($name:expr) => { + let name = $name; + if self.options.fs.is_file(&name) { return Some(name); } + }; + } + + try_path!(path_buf.with_file_name(name).with_extension("scss")); + try_path!(path_buf + .with_file_name(format!("_{}", name.to_str().unwrap())) + .with_extension("scss")); + try_path!(path_buf.clone()); + try_path!(path_buf.join("index.scss")); + try_path!(path_buf.join("_index.scss")); + + for path in &self.options.load_paths { + if self.options.fs.is_dir(path) { + try_path!(path.join(&path_buf) + .with_file_name(name) + .with_extension("scss")); + try_path!(path.join(&path_buf) + .with_file_name(format!("_{}", name.to_str().unwrap())) + .with_extension("scss")); + try_path!(path.join(&path_buf).join("index.scss")); + try_path!(path.join(&path_buf).join("_index.scss")); + } else { + try_path!(path.to_path_buf()); + try_path!(path.with_file_name(name).with_extension("scss")); + try_path!(path + .with_file_name(format!("_{}", name.to_str().unwrap())) + .with_extension("scss")); + try_path!(path.join("index.scss")); + try_path!(path.join("_index.scss")); } } @@ -104,7 +96,7 @@ impl<'a, 'b> Parser<'a, 'b> { if let Some(name) = self.find_import(path) { let file = self.map.add_file( name.to_string_lossy().into(), - String::from_utf8(fs::read(&name)?)?, + String::from_utf8(self.options.fs.read(&name)?)?, ); return Parser { toks: &mut Lexer::new_from_file(&file), diff --git a/src/parse/module.rs b/src/parse/module.rs index ccf41d2..90f9d45 100644 --- a/src/parse/module.rs +++ b/src/parse/module.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fs}; +use std::convert::TryFrom; use codemap::Spanned; @@ -120,9 +120,10 @@ impl<'a, 'b> Parser<'a, 'b> { if let Some(import) = self.find_import(name.as_ref()) { let mut global_scope = Scope::new(); - let file = self - .map - .add_file(name.to_owned(), String::from_utf8(fs::read(&import)?)?); + let file = self.map.add_file( + name.to_owned(), + String::from_utf8(self.options.fs.read(&import)?)?, + ); let mut modules = Modules::default(); diff --git a/src/selector/simple.rs b/src/selector/simple.rs index 98b3bac..ebebe41 100644 --- a/src/selector/simple.rs +++ b/src/selector/simple.rs @@ -569,8 +569,7 @@ impl Pseudo { .any(|pseudo2| self.selector == pseudo2.selector), "nth-child" | "nth-last-child" => compound.components.iter().any(|pseudo2| { if let SimpleSelector::Pseudo( - pseudo - @ Pseudo { + pseudo @ Pseudo { selector: Some(..), .. }, ) = pseudo2 diff --git a/tests/imports.rs b/tests/imports.rs index 798a968..0c5e8fc 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -3,6 +3,25 @@ use std::io::Write; #[macro_use] mod macros; +#[test] +fn null_fs_cannot_import() { + let input = "@import \"foo\";"; + tempfile!("foo.scss", ""); + match grass::from_string( + input.to_string(), + &grass::Options::default().fs(&grass::NullFs), + ) { + Err(e) + if e.to_string() + .starts_with("Error: Can't find stylesheet to import.\n") => + { + () + } + Ok(..) => panic!("did not fail"), + Err(e) => panic!("failed in the wrong way: {}", e), + } +} + #[test] fn imports_variable() { let input = "@import \"imports_variable\";\na {\n color: $a;\n}";