File system interception, and various other matters (#55)

This commit is contained in:
Chris Morgan 2022-02-04 09:41:10 +11:00 committed by GitHub
parent dd92ebf39b
commit 3c5463ac4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 218 additions and 274 deletions

View File

@ -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 doesnt need this feature, its solely to get this `from_string` WASM export.)
# 0.10.8
- bugfix: properly emit the number `0` in compressed mode (#53)

View File

@ -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

70
src/fs.rs Normal file
View File

@ -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: its still using the types and some operations from
/// `std::path`, which constrain it to the target platforms 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<Vec<u8>>;
}
/// 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<Vec<u8>> {
std::fs::read(path)
}
}
/// A file system implementation that acts like its completely empty.
///
/// This may be useful for security as it denies all access to the file system (so `@import` is
/// prevented from leaking anything); youll 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<Vec<u8>> {
Err(Error::new(
ErrorKind::NotFound,
"NullFs, there is no file system",
))
}
}

View File

@ -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<String> {
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<String> {
/// ```
#[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<String> {
from_string_with_file_name(input, "stdin", options)
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn from_string(p: String) -> std::result::Result<String, JsValue> {
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<String, JsValue> {
from_string(Options::default()).map_err(|e| e.to_string())
}

View File

@ -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")

View File

@ -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<Vec<Stmt>> {
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);
}
}

View File

@ -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<PathBuf> = 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),

View File

@ -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();

View File

@ -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

View File

@ -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}";