diff --git a/Cargo.lock b/Cargo.lock index 2701dad..5a74494 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -209,6 +215,7 @@ dependencies = [ "serde_json", "termcolor", "thiserror", + "web-time", ] [[package]] @@ -219,6 +226,26 @@ dependencies = [ "circomspect-program-structure", ] +[[package]] +name = "circomspect-wasm" +version = "0.9.0" +dependencies = [ + "anyhow", + "atty", + "circomspect-parser", + "circomspect-program-analysis", + "circomspect-program-structure", + "clap", + "console_error_panic_hook", + "getrandom", + "js-sys", + "log", + "pretty_env_logger", + "serde_json", + "termcolor", + "wasm-bindgen", +] + [[package]] name = "clap" version = "4.5.7" @@ -284,6 +311,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -446,8 +483,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -537,6 +576,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lalrpop" version = "0.20.2" @@ -1244,6 +1292,71 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.67", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.67", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 7fc7ede..7dd5f9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,5 @@ members = [ "program_analysis", "program_structure", "program_structure_tests", + "wasm", ] diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 6aca1f7..8db75b0 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -15,7 +15,7 @@ mod errors; mod include_logic; mod parser_logic; mod syntax_sugar_traits; -mod syntax_sugar_remover; +pub mod syntax_sugar_remover; pub use parser_logic::parse_definition; diff --git a/parser/src/syntax_sugar_remover.rs b/parser/src/syntax_sugar_remover.rs index 19437d8..da40cad 100644 --- a/parser/src/syntax_sugar_remover.rs +++ b/parser/src/syntax_sugar_remover.rs @@ -17,7 +17,7 @@ use crate::syntax_sugar_traits::ContainsExpression; /// This functions desugars all anonymous components and tuples. #[must_use] -pub(crate) fn remove_syntactic_sugar( +pub fn remove_syntactic_sugar( templates: &HashMap, functions: &HashMap, file_library: &FileLibrary, diff --git a/program_analysis/src/analysis_runner.rs b/program_analysis/src/analysis_runner.rs index 548094d..1899a56 100644 --- a/program_analysis/src/analysis_runner.rs +++ b/program_analysis/src/analysis_runner.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::collections::HashMap; use parser::ParseResult; +use parser::syntax_sugar_remover; use program_structure::{ writers::{LogWriter, ReportWriter}, @@ -14,7 +15,6 @@ use program_structure::{ report::{ReportCollection, Report}, }; -#[cfg(test)] use program_structure::template_library::TemplateLibrary; use crate::{ @@ -76,6 +76,32 @@ impl AnalysisRunner { (self, reports) } + /// Convenience method used for WASM build target. + pub fn with_src_desugar(mut self, file_contents: &[&str]) -> (Self, ReportCollection) { + use parser::parse_definition; + + let mut library_contents = HashMap::new(); + let mut file_library = FileLibrary::default(); + for (file_index, file_source) in file_contents.iter().enumerate() { + let file_name = format!("file-{file_index}.circom"); + let file_id = file_library.add_file(file_name, file_source.to_string(), true); + library_contents.insert(file_id, vec![parse_definition(file_source).unwrap()]); + } + let template_library = TemplateLibrary::new(library_contents, file_library.clone()); + let mut sugar_reports = ReportCollection::new(); + let (new_templates, new_functions) = syntax_sugar_remover::remove_syntactic_sugar( + &template_library.templates, + &template_library.functions, + &template_library.file_library, + &mut sugar_reports, + ); + self.template_asts = new_templates; + self.function_asts = new_functions; + self.file_library = template_library.file_library; + + (self, sugar_reports) + } + /// Convenience method used to generate a runner for testing purposes. #[cfg(test)] pub fn with_src(mut self, file_contents: &[&str]) -> Self { diff --git a/program_structure/Cargo.toml b/program_structure/Cargo.toml index 2d92553..7d58a6a 100644 --- a/program_structure/Cargo.toml +++ b/program_structure/Cargo.toml @@ -28,6 +28,7 @@ serde-sarif = "0.4" serde_json = "1.0" thiserror = "1.0" termcolor = "1.1.3" +web-time = "1.1.0" [dev-dependencies] proptest = "1.1" diff --git a/program_structure/src/control_flow_graph/cfg.rs b/program_structure/src/control_flow_graph/cfg.rs index 9618ad8..d60b9a8 100644 --- a/program_structure/src/control_flow_graph/cfg.rs +++ b/program_structure/src/control_flow_graph/cfg.rs @@ -1,7 +1,7 @@ use log::debug; use std::collections::HashSet; use std::fmt; -use std::time::{Instant, Duration}; +use web_time::{Instant, Duration}; use crate::constants::UsefulConstants; use crate::file_definition::FileID; diff --git a/program_structure/src/program_library/report.rs b/program_structure/src/program_library/report.rs index df5aee8..55328a4 100644 --- a/program_structure/src/program_library/report.rs +++ b/program_structure/src/program_library/report.rs @@ -4,6 +4,7 @@ use std::fmt::Display; use std::str::FromStr; use codespan_reporting::diagnostic::{Diagnostic, Label}; +use serde::{Serialize, Serializer}; use super::report_code::ReportCode; use super::file_definition::{FileID, FileLocation}; @@ -13,7 +14,7 @@ pub type DiagnosticCode = String; pub type ReportLabel = Label; type ReportNote = String; -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] pub enum MessageCategory { Error, Warning, @@ -82,17 +83,39 @@ impl MessageCategory { } } -#[derive(Clone)] +#[derive(Clone, Serialize)] pub struct Report { category: MessageCategory, message: String, primary_file_ids: Vec, + #[serde(with = "label_serde")] primary: Vec, + #[serde(with = "label_serde")] secondary: Vec, notes: Vec, code: ReportCode, } +pub mod label_serde { + use super::*; + + // Serialize the `ReportLabel` (which is `Label`) + pub fn serialize(label: &Vec, serializer: S) -> Result + where + S: Serializer, + { + if label.is_empty() { + return serializer.serialize_str(""); + } + + let start = label[0].range.start as i32; + let end = label[0].range.end as i32; + + serializer.serialize_str(format!("{start},{end}").as_str()) + } + +} + impl Report { fn new(category: MessageCategory, message: String, code: ReportCode) -> Report { Report { diff --git a/program_structure/src/program_library/report_code.rs b/program_structure/src/program_library/report_code.rs index 53e0142..e279b4c 100644 --- a/program_structure/src/program_library/report_code.rs +++ b/program_structure/src/program_library/report_code.rs @@ -1,6 +1,8 @@ +use serde::Serialize; + const DOC_URL: &str = "https://github.com/trailofbits/circomspect/blob/main/doc/analysis_passes.md"; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Serialize)] pub enum ReportCode { AssertWrongType, ParseFail, diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..79a6f72 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "circomspect-wasm" +version = "0.9.0" +edition = "2021" +rust-version = "1.65" +license = "LGPL-3.0-only" +authors = ["Trail of Bits", "numtel "] +readme = "../README.md" +description = "A static analyzer and linter for the Circom zero-knowledge DSL" +keywords = ["cryptography", "static-analysis", "zero-knowledge", "circom"] +repository = "https://github.com/trailofbits/circomspect" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1.0" +atty = "0.2" +clap = { version = "4.5", features = ["derive"] } +log = "0.4" +parser = { package = "circomspect-parser", version = "2.1.3", path = "../parser" } +pretty_env_logger = "0.5" +program_analysis = { package = "circomspect-program-analysis", version = "0.8.1", path = "../program_analysis" } +program_structure = { package = "circomspect-program-structure", version = "2.1.3", path = "../program_structure" } +serde_json = "1.0" +termcolor = "1.1" +wasm-bindgen = "0.2" +console_error_panic_hook = "0.1.7" +js-sys = "0.3" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..d70b7d6 --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,46 @@ +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use js_sys::{Array, JsString}; +extern crate console_error_panic_hook; + +use program_analysis::analysis_runner::AnalysisRunner; +use program_structure::constants::Curve; +use program_structure::writers::{CachedStdoutWriter}; + +#[wasm_bindgen] +pub fn analyze_code(file_content: &JsValue, curve: String) -> String { + console_error_panic_hook::set_once(); + + // Convert incoming JS array or strings to &[&str} for input to analysis_runner + let array = Array::from(file_content); + let vec_of_strings: Vec = array.iter() + .filter_map(|val| val.dyn_into::().ok()) // Convert JsValue to JsString + .map(|js_str| js_str.as_string()) // Convert JsString to String + .filter_map(|opt| opt) // Filter out None values (if any) + .collect(); + let slice_of_strs: Vec<&str> = vec_of_strings.iter().map(|s| s.as_str()).collect(); + let slice: &[&str] = &slice_of_strs; + + let curve = match curve.as_str() { + "BN254" => Curve::Bn254, + "BLS12_381" => Curve::Bls12_381, + "GOLDILOCKS" => Curve::Goldilocks, + _ => Curve::Bn254, + }; + + // Set up analysis runner, passing the file contents directly + let (mut runner, sugar_reports) = AnalysisRunner::new(curve) + .with_src_desugar(&slice); + let mut stdout_writer = CachedStdoutWriter::new(true); + + // Analyze functions and templates in user provided input files. + runner.analyze_functions(&mut stdout_writer, true); + runner.analyze_templates(&mut stdout_writer, true); + + let json_analysis = serde_json::to_string(&stdout_writer.reports()).unwrap(); + let json_sugar_removal_errors = serde_json::to_string(&sugar_reports).unwrap(); + let result = format!("[{},{}]", json_analysis, json_sugar_removal_errors); + + result +} +