diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ec4b4b9b..0fab372d 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,6 +18,8 @@ jobs: - uses: actions/checkout@v3 - name: Add musl target run: rustup target add x86_64-unknown-linux-musl + - name: Add no_std target + run: rustup target add thumbv7m-none-eabi - name: Install musl-gcc run: sudo apt update && sudo apt install -y musl-tools - name: Format Check @@ -26,6 +28,8 @@ jobs: run: cargo build -r --all-features --verbose - name: Build run: cargo build -r --verbose + - name: Build no_std + run: cd tests/ensure_no_std && cargo build -r --target thumbv7m-none-eabi - name: Doc Tests run: cargo test -r --doc - name: Run tests diff --git a/Cargo.toml b/Cargo.toml index 7d753bbf..1401833c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "bindings/wasm", "bindings/java", "bindings/ruby/ext/regorusrb", + "tests/ensure_no_std", ] [package] @@ -33,14 +34,15 @@ crypto = ["dep:constant_time_eq", "dep:hmac", "dep:hex", "dep:md-5", "dep:sha1", deprecated = [] hex = ["dep:data-encoding"] http = [] -jwt = ["dep:jsonwebtoken", "dep:data-encoding"] glob = ["dep:wax"] graph = [] jsonschema = ["dep:jsonschema"] +jwt = ["dep:jsonwebtoken", "dep:data-encoding", "dep:itertools"] +no_std = ["lazy_static/spin_no_std"] opa-runtime = [] regex = ["dep:regex"] semver = ["dep:semver"] -std = ["serde_json/std"] +std = ["rand", "serde_json/std"] time = ["dep:chrono", "dep:chrono-tz"] uuid = ["dep:uuid"] urlquery = ["dep:url"] @@ -67,39 +69,58 @@ full-opa = [ "yaml" ] +# Features that can be used in no_std environments. +# Note that: the spin_no_std feature in lazy_static must be specified. +opa-no-std = [ + "arc", + "base64", + "base64url", + "coverage", + "crypto", + "deprecated", + "graph", + "hex", + "no_std", + "opa-runtime", + "regex", + "semver", + # Configure lazy_static to use spinlocks. + "lazy_static/spin_no_std" +] + # This feature enables some testing utils for OPA tests. opa-testutil = [] [dependencies] -anyhow = { version = "1.0.45", default-features=false } +anyhow = { version = "1.0.45", default-features = false } serde = {version = "1.0.150", default-features = false, features = ["derive", "rc"] } -serde_json = { version = "1.0.89", default-features=false, features = ["alloc"] } -serde_yaml = {version = "0.9.16", optional = true } -lazy_static = "1.4.0" -rand = "0.8.5" -num = "0.4.1" +serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] } +lazy_static = { version = "1.4.0", default-features = false } # Crypto -constant_time_eq = {version = "0.3.0", optional = true} -hmac = {version = "0.12.1", optional = true} -sha2 = {version= "0.10.8", optional = true} -hex = {version = "0.4.3", optional = true} -sha1 = {version = "0.10.6", optional = true} -md-5 = {version = "0.10.6", optional = true} - -data-encoding = { version = "2.4.0", optional = true } +constant_time_eq = {version = "0.3.0", optional = true, default-features = false } +hmac = {version = "0.12.1", optional = true, default-features = false} +sha2 = {version= "0.10.8", optional = true, default-features = false } +hex = {version = "0.4.3", optional = true, default-features = false, features = ["alloc"] } +sha1 = {version = "0.10.6", optional = true, default-features = false } +md-5 = {version = "0.10.6", optional = true, default-features = false } + +data-encoding = { version = "2.4.0", optional = true, default-features=false, features = ["alloc"] } scientific = { version = "0.5.2" } -regex = {version = "1.10.2", optional = true} -semver = {version = "1.0.20", optional = true} +regex = {version = "1.10.2", optional = true, default-features = false } +semver = {version = "1.0.20", optional = true, default-features = false } wax = { version = "0.6.0", features = [], default-features = false, optional = true } url = { version = "2.5.0", optional = true } -uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } +uuid = { version = "1.6.1", default-features = false, features = ["v4", "fast-rng"], optional = true } jsonschema = { version = "0.17.1", default-features = false, optional = true } chrono = { version = "0.4.31", optional = true } chrono-tz = { version = "0.8.5", optional = true } jsonwebtoken = { version = "9.2.0", optional = true } -itertools = "0.12.1" +itertools = { version = "0.12.1", default-features = false, optional = true } + +serde_yaml = {version = "0.9.16", default-features = false, optional = true } +rand = { version = "0.8.5", default-features = false, optional = true } [dev-dependencies] cfg-if = "1.0.0" diff --git a/scripts/pre-push b/scripts/pre-push index 7a2bec87..09577591 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -5,17 +5,24 @@ set -eo pipefail if [ -f Cargo.toml ]; then - # Run precommit checks + # Run precommit checks. dir=$(dirname "${BASH_SOURCE[0]}") "$dir/pre-commit" - # Ensure that the public API works + # Ensure that the public API works. cargo test -r --doc - # Ensure that we can build with all features + # Ensure that no_std build succeeds. + # Build for a target that has no std available. + if command -v rustup > /dev/null; then + rustup target add thumbv7m-none-eabi + (cd tests/ensure_no_std; cargo build -r --target thumbv7m-none-eabi) + fi + + # Ensure that we can build with all features. cargo build -r --all-features - # Ensure that all tests pass + # Ensure that all tests pass. cargo test -r cargo test -r --test aci cargo test -r --test kata diff --git a/src/builtins/debugging.rs b/src/builtins/debugging.rs index 003e1383..1e407080 100644 --- a/src/builtins/debugging.rs +++ b/src/builtins/debugging.rs @@ -43,7 +43,7 @@ pub fn print_to_string( // Additionally interpreter must allow undefined inputs to print. fn print(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { let msg = print_to_string(span, params, args, strict)?; - + let _ = core::convert::identity(&msg); #[cfg(feature = "std")] if !msg.is_empty() { std::eprintln!("{}", &msg[1..]); diff --git a/src/builtins/encoding.rs b/src/builtins/encoding.rs index 6c05a056..0c89ab8a 100644 --- a/src/builtins/encoding.rs +++ b/src/builtins/encoding.rs @@ -63,7 +63,13 @@ fn base64_decode( ensure_args_count(span, name, params, args, 1)?; let encoded_str = ensure_string(name, ¶ms[0], &args[0])?; - let decoded_bytes = data_encoding::BASE64.decode(encoded_str.as_bytes())?; + let decoded_bytes = data_encoding::BASE64 + .decode(encoded_str.as_bytes()) + .map_err(|e| { + params[0] + .span() + .error(&format!("decode failed\nCaused by\n{e}")) + })?; Ok(Value::String( String::from_utf8_lossy(&decoded_bytes).into(), )) @@ -173,7 +179,13 @@ fn hex_decode(span: &Span, params: &[Ref], args: &[Value], _strict: bool) ensure_args_count(span, name, params, args, 1)?; let encoded_str = ensure_string(name, ¶ms[0], &args[0])?; - let decoded_bytes = data_encoding::HEXLOWER_PERMISSIVE.decode(encoded_str.as_bytes())?; + let decoded_bytes = data_encoding::HEXLOWER_PERMISSIVE + .decode(encoded_str.as_bytes()) + .map_err(|e| { + params[0] + .span() + .error(&format!("decode failure\nCaused by\n{e}")) + })?; Ok(Value::String( String::from_utf8_lossy(&decoded_bytes).into(), )) @@ -361,11 +373,9 @@ fn json_is_valid( fn json_marshal(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let name = "json.marshal"; ensure_args_count(span, name, params, args, 1)?; - Ok(Value::String( - serde_json::to_string(&args[0]) - .with_context(|| span.error("could not serialize to json"))? - .into(), - )) + Ok(Value::from(serde_json::to_string(&args[0]).map_err( + |e| span.error(&format!("could not serialize to json\nCaused by\n{e}")), + )?)) } fn json_marshal_with_options( @@ -406,15 +416,13 @@ fn json_marshal_with_options( } if !pretty || options.is_empty() { - return Ok(Value::String( - serde_json::to_string(&args[0]) - .with_context(|| span.error("could not serialize to json"))? - .into(), - )); + return Ok(Value::from(serde_json::to_string(&args[0]).map_err( + |e| span.error(&format!("could not serialize to json\nCaused by\n{e}")), + )?)); } let lines: Vec = serde_json::to_string_pretty(&args[0]) - .with_context(|| span.error("could not serialize to json"))? + .map_err(|e| span.error(&format!("could not serialize to json\nCaused by\n{e}")))? .split('\n') .map(|line| { let mut line = line.to_string(); diff --git a/src/builtins/numbers.rs b/src/builtins/numbers.rs index a3bf55cc..87675386 100644 --- a/src/builtins/numbers.rs +++ b/src/builtins/numbers.rs @@ -3,13 +3,15 @@ use crate::ast::{ArithOp, Expr, Ref}; use crate::builtins; -use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string}; +use crate::builtins::utils::{ensure_args_count, ensure_numeric}; use crate::lexer::Span; use crate::number::Number; use crate::value::Value; use crate::*; use anyhow::{bail, Result}; + +#[cfg(feature = "std")] use rand::{thread_rng, Rng}; pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) { @@ -18,6 +20,7 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn m.insert("floor", (floor, 1)); m.insert("numbers.range", (range, 2)); m.insert("numbers.range_step", (range_step, 3)); + #[cfg(feature = "std")] m.insert("rand.intn", (intn, 2)); m.insert("round", (round, 1)); } @@ -155,10 +158,11 @@ fn round(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re )) } +#[cfg(feature = "std")] fn intn(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { let fcn = "rand.intn"; ensure_args_count(span, fcn, params, args, 2)?; - let _ = ensure_string(fcn, ¶ms[0], &args[0])?; + let _ = crate::builtins::utils::ensure_string(fcn, ¶ms[0], &args[0])?; let n = ensure_numeric(fcn, ¶ms[0], &args[1])?; Ok(match n.as_u64() { diff --git a/src/builtins/opa.rs b/src/builtins/opa.rs index fc388650..b427b779 100644 --- a/src/builtins/opa.rs +++ b/src/builtins/opa.rs @@ -38,6 +38,7 @@ fn opa_runtime(span: &Span, params: &[Ref], args: &[Value], _strict: bool) ); // Emitting environment variables could lead to confidential data being leaked. + #[cfg(feature = "std")] if false { obj.insert( Value::String("env".into()), diff --git a/src/builtins/semver.rs b/src/builtins/semver.rs index 8154fd38..6f4c916d 100644 --- a/src/builtins/semver.rs +++ b/src/builtins/semver.rs @@ -24,8 +24,8 @@ fn compare(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> let v1 = ensure_string(name, ¶ms[0], &args[0])?; let v2 = ensure_string(name, ¶ms[1], &args[1])?; - let version1 = Version::parse(&v1)?; - let version2 = Version::parse(&v2)?; + let version1 = Version::parse(&v1).map_err(|_| params[0].span().error("invalid semver"))?; + let version2 = Version::parse(&v2).map_err(|_| params[0].span().error("invalid semver"))?; let result = match version1.cmp_precedence(&version2) { Ordering::Less => -1, Ordering::Equal => 0, diff --git a/src/builtins/test.rs b/src/builtins/test.rs index 0fe42d06..7a6d5264 100644 --- a/src/builtins/test.rs +++ b/src/builtins/test.rs @@ -7,6 +7,7 @@ use crate::builtins::time; use crate::builtins::utils::{ensure_args_count, ensure_string}; use crate::lexer::Span; use crate::value::Value; +use crate::*; use std::thread; @@ -21,7 +22,8 @@ fn sleep(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re ensure_args_count(span, name, params, args, 1)?; let val = ensure_string(name, ¶ms[0], &args[0])?; - let dur = time::compat::parse_duration(val.as_ref())?; + let dur = time::compat::parse_duration(val.as_ref()) + .map_err(|e| params[0].span().error(&format!("{e}")))?; thread::sleep(dur.to_std()?); diff --git a/src/builtins/time.rs b/src/builtins/time.rs index 12cf4743..8bc2ffa6 100644 --- a/src/builtins/time.rs +++ b/src/builtins/time.rs @@ -147,7 +147,7 @@ fn parse_duration_ns( ensure_args_count(span, name, params, args, 1)?; let value = ensure_string(name, ¶ms[0], &args[0])?; - let dur = compat::parse_duration(value.as_ref())?; + let dur = compat::parse_duration(value.as_ref()).map_err(anyhow::Error::msg)?; safe_timestamp_nanos(span, strict, dur.num_nanoseconds()) } diff --git a/src/builtins/time/compat.rs b/src/builtins/time/compat.rs index e2e0b440..0d491d04 100644 --- a/src/builtins/time/compat.rs +++ b/src/builtins/time/compat.rs @@ -34,7 +34,6 @@ use crate::*; use core::fmt; use core::iter; -use std::error::Error; use chrono::TimeZone; use chrono::{ @@ -72,8 +71,6 @@ impl fmt::Display for ParseDurationError { } } -impl Error for ParseDurationError {} - // Parses a duration string in the form of `10h12m45s`. // // Adapted from Go's `time.ParseDuration`: diff --git a/src/interpreter.rs b/src/interpreter.rs index caa0a299..30d7b059 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -1743,9 +1743,9 @@ impl Interpreter { span.col, format!( "value for key `{}` generated multiple times: `{}` and `{}`", - serde_json::to_string_pretty(&key)?, - serde_json::to_string_pretty(&pv)?, - serde_json::to_string_pretty(&value)?, + serde_json::to_string_pretty(&key).map_err(anyhow::Error::msg)?, + serde_json::to_string_pretty(&pv).map_err(anyhow::Error::msg)?, + serde_json::to_string_pretty(&value).map_err(anyhow::Error::msg)?, ) .as_str(), )); diff --git a/src/lib.rs b/src/lib.rs index f30546c1..a63befbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -404,30 +404,29 @@ pub mod coverage { /// pub fn to_colored_string(&self) -> anyhow::Result { - use std::io::Write; - let mut s = Vec::new(); - writeln!(&mut s, "COVERAGE REPORT:")?; + let mut s = String::default(); + s.push_str("COVERAGE REPORT:\n"); for file in self.files.iter() { if file.not_covered.is_empty() { - writeln!(&mut s, "{} has full coverage", file.path)?; + s.push_str(&format!("{} has full coverage\n", file.path)); continue; } - writeln!(&mut s, "{}:", file.path)?; + s.push_str(&format!("{}:", file.path)); for (line, code) in file.code.split('\n').enumerate() { let line = line as u32 + 1; if file.not_covered.contains(&line) { - writeln!(&mut s, "\x1b[31m {line:4} {code}\x1b[0m")?; + s.push_str(&format!("\x1b[31m {line:4} {code}\x1b[0m\n")); } else if file.covered.contains(&line) { - writeln!(&mut s, "\x1b[32m {line:4} {code}\x1b[0m")?; + s.push_str(&format!("\x1b[32m {line:4} {code}\x1b[0m\n")); } else { - writeln!(&mut s, " {line:4} {code}")?; + s.push_str(&format!(" {line:4} {code}\n")); } } } - writeln!(&mut s)?; - Ok(core::str::from_utf8(&s)?.to_string()) + s.push('\n'); + Ok(s) } } } diff --git a/src/parser.rs b/src/parser.rs index 3f34eab0..e9621f29 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -74,6 +74,7 @@ impl<'source> Parser<'source> { let msg = format!( "`{kw}` will be treated as identifier due to missing `import future.keywords.{kw}`" ); + let _ = core::convert::identity(&msg); #[cfg(feature = "std")] std::println!( "{}", diff --git a/src/scheduler.rs b/src/scheduler.rs index 98f96c52..727266fc 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -47,7 +47,6 @@ pub fn schedule( empty: &Str, ) -> Result { let num_statements = infos.len(); - let orig_infos: Vec<&StmtInfo> = infos.iter().collect(); // Mapping from each var to the list of statements that define it. let mut defining_stmts: BTreeMap> = BTreeMap::new(); @@ -198,7 +197,7 @@ pub fn schedule( if order.len() != num_statements { #[cfg(feature = "std")] - std::eprintln!("could not schedule all statements {order:?} {orig_infos:?}"); + std::eprintln!("could not schedule all statements {order:?}"); return Ok(SortResult::Order( (0..num_statements).map(|i| i as u16).collect(), )); diff --git a/src/value.rs b/src/value.rs index 212aee38..758368ca 100644 --- a/src/value.rs +++ b/src/value.rs @@ -406,6 +406,7 @@ impl Value { /// Deserialize a value from a file containing YAML. /// Note: Deserialization from YAML does not support arbitrary precision numbers. + #[cfg(feature = "std")] #[cfg(feature = "yaml")] pub fn from_yaml_file(path: &String) -> Result { match std::fs::read_to_string(path) { diff --git a/tests/ensure_no_std/Cargo.toml b/tests/ensure_no_std/Cargo.toml new file mode 100644 index 00000000..bcf7745f --- /dev/null +++ b/tests/ensure_no_std/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ensure_no_std" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "1.0.83", default-features = false } +regorus = { path = "../..", default-features = false, features = ["opa-no-std"] } diff --git a/tests/ensure_no_std/src/main.rs b/tests/ensure_no_std/src/main.rs new file mode 100644 index 00000000..d4aff8a4 --- /dev/null +++ b/tests/ensure_no_std/src/main.rs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[no_mangle] +pub extern "C" fn _start() -> ! { + loop {} +}