diff --git a/Cargo.lock b/Cargo.lock index d0334fe3..881ff5ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,27 +257,18 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "candid" -version = "0.9.9" +version = "0.10.0" dependencies = [ "anyhow", - "arbitrary", "bincode", "binread", "byteorder", "candid_derive", "codespan-reporting", - "convert_case", - "crc32fast", "criterion", - "data-encoding", - "fake", - "goldenfile", "hex", - "impls", - "lalrpop", - "lalrpop-util", + "ic_principal", "leb128", - "logos", "num-bigint", "num-traits", "num_enum", @@ -287,12 +278,8 @@ dependencies = [ "serde", "serde_bytes", "serde_cbor", - "serde_dhall", "serde_json", - "serde_test", - "sha2 0.10.8", "stacker", - "test-generator", "thiserror", ] @@ -306,6 +293,34 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "candid_parser" +version = "0.1.0" +dependencies = [ + "anyhow", + "arbitrary", + "candid", + "codespan-reporting", + "convert_case", + "fake", + "goldenfile", + "hex", + "lalrpop", + "lalrpop-util", + "logos", + "num-bigint", + "num-traits", + "pretty 0.12.3", + "rand", + "serde", + "serde_bytes", + "serde_dhall", + "sha2 0.10.8", + "stacker", + "test-generator", + "thiserror", +] + [[package]] name = "cast" version = "0.3.0" @@ -603,6 +618,7 @@ version = "0.3.5" dependencies = [ "anyhow", "candid", + "candid_parser", "clap 4.4.5", "hex", "pretty-hex", @@ -846,6 +862,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ic_principal" +version = "0.1.0" +dependencies = [ + "crc32fast", + "data-encoding", + "hex", + "impls", + "serde", + "serde_bytes", + "serde_cbor", + "serde_json", + "serde_test", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "idna" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 352d28c5..eecbc858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,9 @@ [workspace] members = [ "rust/candid", + "rust/candid_parser", "rust/candid_derive", + "rust/ic_principal", "tools/didc", ] resolver = "2" diff --git a/Changelog.md b/Changelog.md index 51a20b77..d8acf7a6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,13 @@ # Changelog +## Rust 0.10.0 + +* The original `candid` crate is split into three crates: + * `candid`: mainly for Candid data (de-)serialization. + * `candid_parser`: used to be the `parser` and `bindings` module in `candid` crate. + * `ic_principal`: only for `Principal` and `PrincipalError`. + ## Rust 0.9.9 * Set different config values for `full_error_message` and `zero_sized_values` for Wasm and non-Wasm target. diff --git a/rust/candid/Cargo.toml b/rust/candid/Cargo.toml index cd1aa777..a996453b 100644 --- a/rust/candid/Cargo.toml +++ b/rust/candid/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "candid" -version = "0.9.9" +version = "0.10.0" edition = "2021" authors = ["DFINITY Team"] description = "Candid is an interface description language (IDL) for interacting with canisters running on the Internet Computer." @@ -9,22 +9,16 @@ documentation = "https://docs.rs/candid" repository = "https://github.com/dfinity/candid" license = "Apache-2.0" readme = "README.md" - -categories = ["encoding", "parsing", "wasm"] -keywords = ["internet-computer", "idl", "candid", "dfinity", "parser"] -include = ["src", "Cargo.toml", "build.rs", "LICENSE", "README.md"] -build = "build.rs" - -[build-dependencies] -lalrpop = { version = "0.20.0", optional = true } +categories = ["encoding", "wasm"] +keywords = ["internet-computer", "idl", "candid", "dfinity"] +include = ["src", "Cargo.toml", "LICENSE", "README.md"] [dependencies] byteorder = "1.4.3" candid_derive = { path = "../candid_derive", version = "=0.6.3" } codespan-reporting = "0.11" -crc32fast = "1.3.0" -data-encoding = "2.4.0" hex = "0.4.2" +ic_principal = { path = "../ic_principal", version = "0.1.0" } leb128 = "0.2.4" num_enum = "0.6.1" num-bigint = { version = "0.4.2", features = ["serde"] } @@ -33,33 +27,18 @@ paste = "1.0.0" pretty = "0.12.0" serde = { version = "1.0.118", features = ["derive"] } serde_bytes = "0.11" -sha2 = "0.10.1" thiserror = "1.0.20" anyhow = "1.0" binread = { version = "2.1", features = ["debug_template"] } -lalrpop-util = { version = "0.20.0", optional = true } -logos = { version = "0.13", optional = true } -convert_case = { version = "0.6", optional = true } - -arbitrary = { version = "1.0", optional = true } -# Don't upgrade serde_dhall. It will introduce dependency with invalid license. -serde_dhall = { version = "0.11", default-features = false, optional = true } -fake = { version = "2.4", optional = true } -rand = { version = "0.8", optional = true } - [target.'cfg(not(target_arch = "wasm32"))'.dependencies] stacker = "0.1" [dev-dependencies] -goldenfile = "1.1.0" -test-generator = "0.3.0" rand = "0.8" criterion = "0.4" serde_cbor = "0.11.2" serde_json = "1.0.74" -serde_test = "1.0.137" -impls = "1" bincode = "1.3.3" [[bench]] @@ -67,33 +46,5 @@ name = "benchmark" harness = false path = "benches/benchmark.rs" -[[test]] -name = "test_suite" -path = "tests/test_suite.rs" -required-features = ["parser"] -[[test]] -name = "value" -path = "tests/value.rs" -required-features = ["parser"] -[[test]] -name = "parse_value" -path = "tests/parse_value.rs" -required-features = ["parser"] -[[test]] -name = "parse_type" -path = "tests/parse_type.rs" -required-features = ["parser"] - [features] -configs = ["serde_dhall"] -random = ["parser", "configs", "arbitrary", "fake", "rand"] -parser = ["lalrpop", "lalrpop-util", "logos", "convert_case"] -all = ["random"] mute_warnings = [] - -# docs.rs-specific configuration -# To test locally: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features all -[package.metadata.docs.rs] -features = ["all"] -# defines the configuration attribute `docsrs` -rustdoc-args = ["--cfg", "docsrs"] diff --git a/rust/candid/src/bindings/mod.rs b/rust/candid/src/bindings/mod.rs deleted file mode 100644 index 89d86ab3..00000000 --- a/rust/candid/src/bindings/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Candid bindings for different languages. -// This module assumes the input are type checked, it is safe to use unwrap. - -pub mod candid; - -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod analysis; -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod javascript; -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod motoko; -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod rust; -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod typescript; diff --git a/rust/candid/src/error.rs b/rust/candid/src/error.rs index 9181d4a0..095b8208 100644 --- a/rust/candid/src/error.rs +++ b/rust/candid/src/error.rs @@ -2,27 +2,13 @@ use codespan_reporting::diagnostic::Label; use serde::{de, ser}; -use std::io; +use std::{io, num::ParseIntError}; use thiserror::Error; -#[cfg(feature = "parser")] -use crate::parser::token; -#[cfg(feature = "parser")] -use codespan_reporting::{ - diagnostic::Diagnostic, - files::{Error as ReportError, SimpleFile}, - term::{self, termcolor::StandardStream}, -}; - pub type Result = std::result::Result; #[derive(Debug, Error)] pub enum Error { - #[cfg_attr(docsrs, doc(cfg(feature = "parser")))] - #[cfg(feature = "parser")] - #[error("Candid parser error: {0}")] - Parse(#[from] token::ParserError), - #[error("binary parser error: {}", .0.get(0).map(|f| format!("{} at byte offset {}", f.message, f.range.start/2)).unwrap_or_else(|| "io error".to_string()))] Binread(Vec>), @@ -40,42 +26,6 @@ impl Error { pub fn subtype(msg: T) -> Self { Error::Subtype(msg.to_string()) } - #[cfg_attr(docsrs, doc(cfg(feature = "parser")))] - #[cfg(feature = "parser")] - pub fn report(&self) -> Diagnostic<()> { - match self { - Error::Parse(e) => { - use lalrpop_util::ParseError::*; - let mut diag = Diagnostic::error().with_message("parser error"); - let label = match e { - User { error } => { - Label::primary((), error.span.clone()).with_message(&error.err) - } - InvalidToken { location } => { - Label::primary((), *location..location + 1).with_message("Invalid token") - } - UnrecognizedEof { location, expected } => { - diag = diag.with_notes(report_expected(expected)); - Label::primary((), *location..location + 1).with_message("Unexpected EOF") - } - UnrecognizedToken { token, expected } => { - diag = diag.with_notes(report_expected(expected)); - Label::primary((), token.0..token.2).with_message("Unexpected token") - } - ExtraToken { token } => { - Label::primary((), token.0..token.2).with_message("Extra token") - } - }; - diag.with_labels(vec![label]) - } - Error::Binread(labels) => { - let diag = Diagnostic::error().with_message("decoding error"); - diag.with_labels(labels.to_vec()) - } - Error::Subtype(e) => Diagnostic::error().with_message(e), - Error::Custom(e) => Diagnostic::error().with_message(e.to_string()), - } - } } fn get_binread_labels(e: &binread::Error) -> Vec> { @@ -123,26 +73,6 @@ fn get_binread_labels(e: &binread::Error) -> Vec> { } } -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -fn report_expected(expected: &[String]) -> Vec { - if expected.is_empty() { - return Vec::new(); - } - use pretty::RcDoc; - let doc: RcDoc<()> = RcDoc::intersperse( - expected.iter().map(RcDoc::text), - RcDoc::text(",").append(RcDoc::softline()), - ); - let header = if expected.len() == 1 { - "Expects" - } else { - "Expects one of" - }; - let doc = RcDoc::text(header).append(RcDoc::softline().append(doc)); - vec![doc.pretty(70).to_string()] -} - impl ser::Error for Error { fn custom(msg: T) -> Self { Error::msg(format!("Serialize error: {msg}")) @@ -174,56 +104,9 @@ impl From for Error { Error::Binread(get_binread_labels(&e)) } } -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -impl From for Error { - fn from(e: ReportError) -> Error { - Error::msg(e) - } -} -#[cfg_attr(docsrs, doc(cfg(feature = "random")))] -#[cfg(feature = "random")] -impl From for Error { - fn from(e: arbitrary::Error) -> Error { - Error::msg(format!("arbitrary error: {e}")) - } -} -#[cfg_attr(docsrs, doc(cfg(feature = "configs")))] -#[cfg(feature = "configs")] -impl From for Error { - fn from(e: serde_dhall::Error) -> Error { - Error::msg(format!("dhall error: {e}")) +impl From for Error { + fn from(e: ParseIntError) -> Error { + Error::msg(format!("ParseIntError: {e}")) } } - -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub fn pretty_parse(name: &str, str: &str) -> Result -where - T: std::str::FromStr, -{ - str.parse::().or_else(|e| { - let writer = StandardStream::stderr(term::termcolor::ColorChoice::Auto); - let config = term::Config::default(); - let file = SimpleFile::new(name, str); - term::emit(&mut writer.lock(), &config, &file, &e.report())?; - Err(e) - }) -} -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub fn pretty_read(reader: &mut std::io::Cursor<&[u8]>) -> Result -where - T: binread::BinRead, -{ - T::read(reader).or_else(|e| { - let e = Error::from(e); - let writer = StandardStream::stderr(term::termcolor::ColorChoice::Auto); - let config = term::Config::default(); - let str = hex::encode(reader.get_ref()); - let file = SimpleFile::new("binary", &str); - term::emit(&mut writer.lock(), &config, &file, &e.report())?; - Err(e) - }) -} diff --git a/rust/candid/src/lib.rs b/rust/candid/src/lib.rs index 446735af..c4d5217a 100644 --- a/rust/candid/src/lib.rs +++ b/rust/candid/src/lib.rs @@ -170,111 +170,6 @@ //! # Ok::<(), candid::Error>(()) //! ``` //! -//! We provide a data structure [`candid::IDLArgs`](parser/value/struct.IDLArgs.html) to represent a sequence of `IDLValue`s, -//! and use `to_bytes()` and `from_bytes()` to encode and decode Candid messages. -//! We also provide a parser to parse Candid values in text format. -//! -//! ``` -//! #[cfg(feature = "parser")] -//! # fn f() -> Result<(), candid::Error> { -//! use candid::{IDLArgs, TypeEnv}; -//! // Candid values represented in text format -//! let text_value = r#" -//! (42, opt true, vec {1;2;3}, -//! opt record {label="text"; 42="haha"}) -//! "#; -//! -//! // Parse text format into IDLArgs for serialization -//! let args: IDLArgs = text_value.parse()?; -//! let encoded: Vec = args.to_bytes()?; -//! -//! // Deserialize into IDLArgs -//! let decoded: IDLArgs = IDLArgs::from_bytes(&encoded)?; -//! assert_eq!(encoded, decoded.to_bytes()?); -//! -//! // Convert IDLArgs to text format -//! let output: String = decoded.to_string(); -//! let parsed_args: IDLArgs = output.parse()?; -//! let annotated_args = args.annotate_types(true, &TypeEnv::new(), &parsed_args.get_types())?; -//! assert_eq!(annotated_args, parsed_args); -//! # Ok(()) -//! # } -//! ``` -//! Note that when parsing Candid values, we assume the number literals are always of type `Int`. -//! This can be changed by providing the type of the method arguments, which can usually be obtained -//! by parsing a Candid file in the following section. -//! -//! ## Operating on Candid AST -//! We provide a parser and type checker for Candid files specifying the service interface. -//! -//! ``` -//! #[cfg(feature = "parser")] -//! # fn f() -> Result<(), candid::Error> { -//! use candid::{IDLProg, TypeEnv, check_prog, types::{Type, TypeInner}}; -//! let did_file = r#" -//! type List = opt record { head: int; tail: List }; -//! type byte = nat8; -//! service : { -//! f : (byte, int, nat, int8) -> (List); -//! g : (List) -> (int) query; -//! } -//! "#; -//! -//! // Parse did file into an AST -//! let ast: IDLProg = did_file.parse()?; -//! -//! // Type checking a given .did file -//! // let (env, opt_actor) = check_file("a.did")?; -//! // Or alternatively, use check_prog to check in-memory did file -//! // Note that file import is ignored by check_prog. -//! let mut env = TypeEnv::new(); -//! let actor: Type = check_prog(&mut env, &ast)?.unwrap(); -//! -//! let method = env.get_method(&actor, "g").unwrap(); -//! assert_eq!(method.is_query(), true); -//! assert_eq!(method.args, vec![TypeInner::Var("List".to_string()).into()]); -//! # Ok(()) -//! # } -//! ``` -//! -//! ## Serializing untyped Candid values with type annotations. -//! With type signatures from the Candid file, [`candid::IDLArgs`](parser/value/struct.IDLArgs.html) -//! uses `to_bytes_with_types` function to serialize arguments directed by the Candid types. -//! This is useful when serializing different number types and recursive types. -//! There is no need to use types for deserialization as the types are available in the Candid message. -//! -//! ``` -//! #[cfg(feature = "parser")] -//! # fn f() -> Result<(), candid::Error> { -//! use candid::{IDLArgs, types::value::IDLValue}; -//! # use candid::{IDLProg, TypeEnv, check_prog}; -//! # let did_file = r#" -//! # type List = opt record { head: int; tail: List }; -//! # type byte = nat8; -//! # service : { -//! # f : (byte, int, nat, int8) -> (List); -//! # g : (List) -> (int) query; -//! # } -//! # "#; -//! # let ast = did_file.parse::()?; -//! # let mut env = TypeEnv::new(); -//! # let actor = check_prog(&mut env, &ast)?.unwrap(); -//! // Get method type f : (byte, int, nat, int8) -> (List) -//! let method = env.get_method(&actor, "f").unwrap(); -//! let args = "(42, 42, 42, 42)".parse::()?; -//! // Serialize arguments with candid types -//! let encoded = args.to_bytes_with_types(&env, &method.args)?; -//! let decoded = IDLArgs::from_bytes(&encoded)?; -//! assert_eq!(decoded.args, -//! vec![IDLValue::Nat8(42), -//! IDLValue::Int(42.into()), -//! IDLValue::Nat(42.into()), -//! IDLValue::Int8(42) -//! ]); -//! # Ok(()) -//! # } -//! ``` -//! //! ## Building the library as a JS/Wasm package //! With the help of `wasm-bindgen` and `wasm-pack`, we can build the library as a Wasm binary and run in the browser. //! This is useful for client-side UIs and parsing did files in JavaScript. @@ -287,6 +182,7 @@ //! [dependencies] //! wasm-bindgen = "0.2" //! candid = "0.9.0" +//! candid_parser = "0.1.0" //! //! [profile.release] //! lto = true @@ -294,14 +190,15 @@ //! ``` //! Expose the methods in `lib.rs` //! ```ignore -//! use candid::{check_prog, IDLProg, TypeEnv}; +//! use candid::TypeEnv; +//! use candid_parser::{check_prog, IDLProg}; //! use wasm_bindgen::prelude::*; //! #[wasm_bindgen] //! pub fn did_to_js(prog: String) -> Option { //! let ast = prog.parse::().ok()?; //! let mut env = TypeEnv::new(); //! let actor = check_prog(&mut env, &ast).ok()?; -//! Some(candid::bindings::javascript::compile(&env, &actor)) +//! Some(candid_parser::bindings::javascript::compile(&env, &actor)) //! } //! ``` //! ### Building @@ -353,18 +250,7 @@ pub mod utils; pub use utils::{decode_args, decode_one, encode_args, encode_one, write_args}; pub mod pretty; -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub mod parser; -#[cfg(feature = "parser")] -pub use error::{pretty_parse, pretty_read}; -#[cfg(feature = "parser")] -pub use parser::{ - types::IDLProg, - typing::{check_file, check_prog, pretty_check_file}, -}; - -pub mod bindings; +pub mod pretty_printer; // Candid hash function comes from // https://caml.inria.fr/pub/papers/garrigue-polymorphic_variants-ml98.pdf diff --git a/rust/candid/src/parser/grammar.rs b/rust/candid/src/parser/grammar.rs deleted file mode 100644 index 2f39e7f0..00000000 --- a/rust/candid/src/parser/grammar.rs +++ /dev/null @@ -1,2 +0,0 @@ -#![allow(clippy::all)] -include!(concat!(env!("OUT_DIR"), "/parser/grammar.rs")); diff --git a/rust/candid/src/parser/mod.rs b/rust/candid/src/parser/mod.rs deleted file mode 100644 index 6c4c399c..00000000 --- a/rust/candid/src/parser/mod.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Provides parser for Candid type and value. -//! * `str.parse::()` parses the Candid signature file to Candid AST. -//! * `str.parse::()` parses the Candid value in text format to a struct `IDLArg` that can be used for serialization and deserialization between Candid and an enum type `IDLValue` in Rust. - -pub mod grammar; - -pub mod token; -pub mod types; - -pub mod typing; - -#[cfg_attr(docsrs, doc(cfg(feature = "configs")))] -#[cfg(feature = "configs")] -pub mod configs; -#[cfg_attr(docsrs, doc(cfg(feature = "random")))] -#[cfg(feature = "random")] -pub mod random; -pub mod test; - -pub use crate::types::value::{IDLArgs, IDLValue}; - -impl std::str::FromStr for IDLArgs { - type Err = crate::Error; - fn from_str(str: &str) -> std::result::Result { - let lexer = token::Tokenizer::new(str); - Ok(grammar::ArgsParser::new().parse(lexer)?) - } -} - -impl std::str::FromStr for IDLValue { - type Err = crate::Error; - fn from_str(str: &str) -> std::result::Result { - let lexer = token::Tokenizer::new(str); - Ok(grammar::ArgParser::new().parse(lexer)?) - } -} diff --git a/rust/candid/src/bindings/candid.rs b/rust/candid/src/pretty_printer.rs similarity index 97% rename from rust/candid/src/bindings/candid.rs rename to rust/candid/src/pretty_printer.rs index ffe4d8d3..fd513436 100644 --- a/rust/candid/src/bindings/candid.rs +++ b/rust/candid/src/pretty_printer.rs @@ -39,7 +39,7 @@ fn is_keyword(id: &str) -> bool { KEYWORDS.contains(&id) } -pub(crate) fn is_valid_as_id(id: &str) -> bool { +pub fn is_valid_as_id(id: &str) -> bool { if id.is_empty() || !id.is_ascii() { return false; } @@ -161,7 +161,7 @@ pub fn pp_args(args: &[Type]) -> RcDoc { enclose("(", doc, ")") } -pub(crate) fn pp_modes(modes: &[crate::types::FuncMode]) -> RcDoc { +pub fn pp_modes(modes: &[crate::types::FuncMode]) -> RcDoc { RcDoc::concat(modes.iter().map(|m| RcDoc::space().append(m.to_doc()))) } @@ -211,7 +211,7 @@ pub fn compile(env: &TypeEnv, actor: &Option) -> String { } pub mod value { - use crate::bindings::candid::{ident_string, pp_text}; + use super::{ident_string, pp_text}; use crate::pretty::*; use crate::types::value::{IDLArgs, IDLField, IDLValue}; use crate::types::{number::pp_num_str, Label}; @@ -325,12 +325,7 @@ pub mod value { Reserved => write!(f, "null : reserved"), Principal(id) => write!(f, "principal \"{id}\""), Service(id) => write!(f, "service \"{id}\""), - Func(id, meth) => write!( - f, - "func \"{}\".{}", - id, - crate::bindings::candid::ident_string(meth) - ), + Func(id, meth) => write!(f, "func \"{}\".{}", id, ident_string(meth)), Opt(v) if has_type_annotation(v) => write!(f, "opt ({v:?})"), Opt(v) => write!(f, "opt {v:?}"), Vec(vs) => { diff --git a/rust/candid/src/types/internal.rs b/rust/candid/src/types/internal.rs index 47676e07..9c296d36 100644 --- a/rust/candid/src/types/internal.rs +++ b/rust/candid/src/types/internal.rs @@ -283,16 +283,12 @@ impl Type { } impl fmt::Display for Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", crate::bindings::candid::pp_ty(self).pretty(80)) + write!(f, "{}", crate::pretty_printer::pp_ty(self).pretty(80)) } } impl fmt::Display for TypeInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - crate::bindings::candid::pp_ty_inner(self).pretty(80) - ) + write!(f, "{}", crate::pretty_printer::pp_ty_inner(self).pretty(80)) } } pub(crate) fn text_size(t: &Type, limit: i32) -> Result { @@ -410,7 +406,7 @@ impl fmt::Display for Field { write!( f, "{}", - crate::bindings::candid::pp_field(self, false).pretty(80) + crate::pretty_printer::pp_field(self, false).pretty(80) ) } } @@ -462,7 +458,7 @@ pub enum FuncMode { CompositeQuery, } impl FuncMode { - pub(crate) fn to_doc(&self) -> pretty::RcDoc { + pub fn to_doc(&self) -> pretty::RcDoc { match self { FuncMode::Oneway => pretty::RcDoc::text("oneway"), FuncMode::Query => pretty::RcDoc::text("query"), @@ -478,11 +474,7 @@ pub struct Function { } impl fmt::Display for Function { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - crate::bindings::candid::pp_function(self).pretty(80) - ) + write!(f, "{}", crate::pretty_printer::pp_function(self).pretty(80)) } } impl Function { @@ -528,7 +520,7 @@ macro_rules! service { #[derive(Debug, PartialEq, TryFromPrimitive)] #[repr(i64)] -pub(crate) enum Opcode { +pub enum Opcode { Null = -1, Bool = -2, Nat = -3, @@ -607,7 +599,7 @@ thread_local! { static NAME: RefCell = RefCell::new(Default::default()); } -pub(crate) fn find_type(id: &TypeId) -> Option { +pub fn find_type(id: &TypeId) -> Option { ENV.with(|e| e.borrow().get(id).cloned()) } @@ -620,7 +612,7 @@ pub(crate) fn show_env() { pub(crate) fn env_add(id: TypeId, t: Type) { ENV.with(|e| drop(e.borrow_mut().insert(id, t))); } -pub(crate) fn env_clear() { +pub fn env_clear() { ENV.with(|e| e.borrow_mut().clear()); } diff --git a/rust/candid/src/types/number.rs b/rust/candid/src/types/number.rs index ef550bea..75ca3de3 100644 --- a/rust/candid/src/types/number.rs +++ b/rust/candid/src/types/number.rs @@ -88,7 +88,7 @@ impl std::str::FromStr for Nat { } } -pub(crate) fn pp_num_str(s: &str) -> String { +pub fn pp_num_str(s: &str) -> String { let mut groups = Vec::new(); for chunk in s.as_bytes().rchunks(3) { let str = String::from_utf8_lossy(chunk); diff --git a/rust/candid/src/types/principal.rs b/rust/candid/src/types/principal.rs index f5c8d776..7cca8fbd 100644 --- a/rust/candid/src/types/principal.rs +++ b/rust/candid/src/types/principal.rs @@ -1,95 +1,6 @@ use super::{CandidType, Serializer, Type, TypeInner}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha224}; -use std::convert::TryFrom; -use std::fmt::Write; -use thiserror::Error; -/// An error happened while encoding, decoding or serializing a [`Principal`]. -#[derive(Error, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum PrincipalError { - #[error("Bytes is longer than 29 bytes.")] - BytesTooLong(), - - #[error("Text must be in valid Base32 encoding.")] - InvalidBase32(), - - #[error("Text is too short.")] - TextTooShort(), - - #[error("Text is too long.")] - TextTooLong(), - - #[error("CRC32 check sequence doesn't match with calculated from Principal bytes.")] - CheckSequenceNotMatch(), - - #[error(r#"Text should be separated by - (dash) every 5 characters: expected "{0}""#)] - AbnormalGrouped(Principal), -} - -/// Generic ID on Internet Computer. -/// -/// Principals are generic identifiers for canisters, users -/// and possibly other concepts in the future. -/// As far as most uses of the IC are concerned they are -/// opaque binary blobs with a length between 0 and 29 bytes, -/// and there is intentionally no mechanism to tell canister ids and user ids apart. -/// -/// Note a principal is not necessarily tied with a public key-pair, -/// yet we need at least a key-pair of a related principal to sign -/// requests. -/// -/// A Principal can be serialized to a byte array ([`Vec`]) or a text -/// representation, but the inner structure of the byte representation -/// is kept private. -/// -/// Example of using a Principal object: -/// ``` -/// # use candid::Principal; -/// let text = "aaaaa-aa"; // The management canister ID. -/// let principal = Principal::from_text(text).expect("Could not decode the principal."); -/// assert_eq!(principal.as_slice(), &[] as &[u8]); -/// assert_eq!(principal.to_text(), text); -/// ``` -/// -/// Similarly, serialization using serde has two versions: -/// serilizing to a byte bufer for non-human readable serializer, and a string version for human -/// readable serializers. -/// -/// ``` -/// # use candid::Principal; -/// use serde::{Deserialize, Serialize}; -/// use std::str::FromStr; -/// -/// #[derive(Serialize)] -/// struct Data { -/// id: Principal, -/// } -/// -/// let id = Principal::from_str("2chl6-4hpzw-vqaaa-aaaaa-c").unwrap(); -/// -/// // JSON is human readable, so this will serialize to a textual -/// // representation of the Principal. -/// assert_eq!( -/// serde_json::to_string(&Data { id: id.clone() }).unwrap(), -/// r#"{"id":"2chl6-4hpzw-vqaaa-aaaaa-c"}"# -/// ); -/// -/// // CBOR is not human readable, so will serialize to bytes. -/// assert_eq!( -/// serde_cbor::to_vec(&Data { id: id.clone() }).unwrap(), -/// &[161, 98, 105, 100, 73, 239, 205, 171, 0, 0, 0, 0, 0, 1], -/// ); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Principal { - /// Length. - len: u8, - - /// The content buffer. When returning slices this should always be sized according to - /// `len`. - bytes: [u8; Self::MAX_LENGTH_IN_BYTES], -} +pub use ic_principal::{Principal, PrincipalError}; impl CandidType for Principal { fn _ty() -> Type { @@ -102,266 +13,3 @@ impl CandidType for Principal { serializer.serialize_principal(self.as_slice()) } } - -impl Principal { - const MAX_LENGTH_IN_BYTES: usize = 29; - const CRC_LENGTH_IN_BYTES: usize = 4; - - const SELF_AUTHENTICATING_TAG: u8 = 2; - const ANONYMOUS_TAG: u8 = 4; - - /// Construct a [`Principal`] of the IC management canister - pub const fn management_canister() -> Self { - Self { - len: 0, - bytes: [0; Self::MAX_LENGTH_IN_BYTES], - } - } - - /// Construct a self-authenticating ID from public key - pub fn self_authenticating>(public_key: P) -> Self { - let public_key = public_key.as_ref(); - let hash = Sha224::digest(public_key); - let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; - bytes[..Self::MAX_LENGTH_IN_BYTES - 1].copy_from_slice(hash.as_slice()); - bytes[Self::MAX_LENGTH_IN_BYTES - 1] = Self::SELF_AUTHENTICATING_TAG; - - Self { - len: Self::MAX_LENGTH_IN_BYTES as u8, - bytes, - } - } - - /// Construct an anonymous ID. - pub const fn anonymous() -> Self { - let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; - bytes[0] = Self::ANONYMOUS_TAG; - Self { len: 1, bytes } - } - - /// Construct a [`Principal`] from a slice of bytes. - /// - /// # Panics - /// - /// Panics if the slice is longer than 29 bytes. - pub const fn from_slice(slice: &[u8]) -> Self { - match Self::try_from_slice(slice) { - Ok(v) => v, - _ => panic!("slice length exceeds capacity"), - } - } - - /// Construct a [`Principal`] from a slice of bytes. - pub const fn try_from_slice(slice: &[u8]) -> Result { - const MAX_LENGTH_IN_BYTES: usize = Principal::MAX_LENGTH_IN_BYTES; - match slice.len() { - len @ 0..=MAX_LENGTH_IN_BYTES => { - let mut bytes = [0; MAX_LENGTH_IN_BYTES]; - let mut i = 0; - while i < len { - bytes[i] = slice[i]; - i += 1; - } - Ok(Self { - len: len as u8, - bytes, - }) - } - _ => Err(PrincipalError::BytesTooLong()), - } - } - - /// Parse a [`Principal`] from text representation. - pub fn from_text>(text: S) -> Result { - // Strategy: Parse very liberally, then pretty-print and compare output - // This is both simpler and yields better error messages - - let mut s = text.as_ref().to_string(); - s.make_ascii_uppercase(); - s.retain(|c| c != '-'); - match data_encoding::BASE32_NOPAD.decode(s.as_bytes()) { - Ok(bytes) => { - if bytes.len() < Self::CRC_LENGTH_IN_BYTES { - return Err(PrincipalError::TextTooShort()); - } - - let crc_bytes = &bytes[..Self::CRC_LENGTH_IN_BYTES]; - let data_bytes = &bytes[Self::CRC_LENGTH_IN_BYTES..]; - if data_bytes.len() > Self::MAX_LENGTH_IN_BYTES { - return Err(PrincipalError::TextTooLong()); - } - - if crc32fast::hash(data_bytes).to_be_bytes() != crc_bytes { - return Err(PrincipalError::CheckSequenceNotMatch()); - } - - // Already checked data_bytes.len() <= MAX_LENGTH_IN_BYTES - // safe to unwrap here - let result = Self::try_from_slice(data_bytes).unwrap(); - let expected = format!("{result}"); - - // In the Spec: - // The textual representation is conventionally printed with lower case letters, - // but parsed case-insensitively. - if text.as_ref().to_ascii_lowercase() != expected { - return Err(PrincipalError::AbnormalGrouped(result)); - } - Ok(result) - } - _ => Err(PrincipalError::InvalidBase32()), - } - } - - /// Convert [`Principal`] to text representation. - pub fn to_text(&self) -> String { - format!("{self}") - } - - /// Return the [`Principal`]'s underlying slice of bytes. - #[inline] - pub fn as_slice(&self) -> &[u8] { - &self.bytes[..self.len as usize] - } -} - -impl std::fmt::Display for Principal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let blob: &[u8] = self.as_slice(); - - // calc checksum - let checksum = crc32fast::hash(blob); - - // combine blobs - let mut bytes = vec![]; - bytes.extend_from_slice(&checksum.to_be_bytes()); - bytes.extend_from_slice(blob); - - // base32 - let mut s = data_encoding::BASE32_NOPAD.encode(&bytes); - s.make_ascii_lowercase(); - - // write out string with dashes - let mut s = s.as_str(); - while s.len() > 5 { - f.write_str(&s[..5])?; - f.write_char('-')?; - s = &s[5..]; - } - f.write_str(s) - } -} - -impl std::str::FromStr for Principal { - type Err = PrincipalError; - - fn from_str(s: &str) -> Result { - Principal::from_text(s) - } -} - -impl TryFrom<&str> for Principal { - type Error = PrincipalError; - - fn try_from(s: &str) -> Result { - Principal::from_text(s) - } -} - -impl TryFrom> for Principal { - type Error = PrincipalError; - - fn try_from(bytes: Vec) -> Result { - Self::try_from(bytes.as_slice()) - } -} - -impl TryFrom<&Vec> for Principal { - type Error = PrincipalError; - - fn try_from(bytes: &Vec) -> Result { - Self::try_from(bytes.as_slice()) - } -} - -impl TryFrom<&[u8]> for Principal { - type Error = PrincipalError; - - fn try_from(bytes: &[u8]) -> Result { - Self::try_from_slice(bytes) - } -} - -impl AsRef<[u8]> for Principal { - fn as_ref(&self) -> &[u8] { - self.as_slice() - } -} - -// Serialization -impl serde::Serialize for Principal { - fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - self.to_text().serialize(serializer) - } else { - serializer.serialize_bytes(self.as_slice()) - } - } -} - -// Deserialization -mod deserialize { - use super::Principal; - use std::convert::TryFrom; - - // Simple visitor for deserialization from bytes. We don't support other number types - // as there's no need for it. - pub(super) struct PrincipalVisitor; - - impl<'de> serde::de::Visitor<'de> for PrincipalVisitor { - type Value = super::Principal; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("bytes or string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - Principal::from_text(v).map_err(E::custom) - } - - fn visit_bytes(self, value: &[u8]) -> Result - where - E: serde::de::Error, - { - Principal::try_from(value).map_err(E::custom) - } - /// This visitor should only be used by the Candid crate. - fn visit_byte_buf(self, v: Vec) -> Result - where - E: serde::de::Error, - { - if v.is_empty() || v[0] != 2u8 { - Err(E::custom("Not called by Candid")) - } else { - Principal::try_from(&v[1..]).map_err(E::custom) - } - } - } -} - -impl<'de> serde::Deserialize<'de> for Principal { - fn deserialize>(deserializer: D) -> Result { - use serde::de::Error; - if deserializer.is_human_readable() { - deserializer - .deserialize_str(deserialize::PrincipalVisitor) - .map_err(D::Error::custom) - } else { - deserializer - .deserialize_bytes(deserialize::PrincipalVisitor) - .map_err(D::Error::custom) - } - } -} diff --git a/rust/candid/src/types/subtype.rs b/rust/candid/src/types/subtype.rs index 552c9b85..85c6e334 100644 --- a/rust/candid/src/types/subtype.rs +++ b/rust/candid/src/types/subtype.rs @@ -1,5 +1,5 @@ use super::internal::{find_type, Field, Label, Type, TypeInner}; -use crate::bindings::candid::pp_args; +use crate::pretty_printer::pp_args; use crate::types::TypeEnv; use crate::{Error, Result}; use anyhow::Context; diff --git a/rust/candid/src/types/value.rs b/rust/candid/src/types/value.rs index 89ac3cf5..ebfb0f5c 100644 --- a/rust/candid/src/types/value.rs +++ b/rust/candid/src/types/value.rs @@ -134,10 +134,6 @@ impl IDLValue { /// string, we need to set `from_parser` to true to enable converting numbers to the expected /// types, and disable the opt rules. pub fn annotate_type(&self, from_parser: bool, env: &TypeEnv, t: &Type) -> Result { - #[cfg(not(feature = "parser"))] - if from_parser { - panic!("Please enable \"parser\" feature"); - } Ok(match (self, t.as_ref()) { (_, TypeInner::Var(id)) => { let ty = env.rec_find_type(id)?; @@ -239,27 +235,23 @@ impl IDLValue { (IDLValue::Principal(id), TypeInner::Principal) => IDLValue::Principal(*id), (IDLValue::Service(_), TypeInner::Service(_)) => self.clone(), (IDLValue::Func(_, _), TypeInner::Func(_)) => self.clone(), - #[cfg(feature = "parser")] - (IDLValue::Number(str), _) if from_parser => { - use crate::parser::token::error; - match t.as_ref() { - TypeInner::Int => IDLValue::Int(str.parse::()?), - TypeInner::Nat => IDLValue::Nat(str.parse::()?), - TypeInner::Nat8 => IDLValue::Nat8(str.parse::().map_err(error)?), - TypeInner::Nat16 => IDLValue::Nat16(str.parse::().map_err(error)?), - TypeInner::Nat32 => IDLValue::Nat32(str.parse::().map_err(error)?), - TypeInner::Nat64 => IDLValue::Nat64(str.parse::().map_err(error)?), - TypeInner::Int8 => IDLValue::Int8(str.parse::().map_err(error)?), - TypeInner::Int16 => IDLValue::Int16(str.parse::().map_err(error)?), - TypeInner::Int32 => IDLValue::Int32(str.parse::().map_err(error)?), - TypeInner::Int64 => IDLValue::Int64(str.parse::().map_err(error)?), - _ => { - return Err(Error::msg(format!( - "type mismatch: {self} can not be of type {t}" - ))) - } + (IDLValue::Number(str), _) if from_parser => match t.as_ref() { + TypeInner::Int => IDLValue::Int(str.parse::()?), + TypeInner::Nat => IDLValue::Nat(str.parse::()?), + TypeInner::Nat8 => IDLValue::Nat8(str.parse::()?), + TypeInner::Nat16 => IDLValue::Nat16(str.parse::()?), + TypeInner::Nat32 => IDLValue::Nat32(str.parse::()?), + TypeInner::Nat64 => IDLValue::Nat64(str.parse::()?), + TypeInner::Int8 => IDLValue::Int8(str.parse::()?), + TypeInner::Int16 => IDLValue::Int16(str.parse::()?), + TypeInner::Int32 => IDLValue::Int32(str.parse::()?), + TypeInner::Int64 => IDLValue::Int64(str.parse::()?), + _ => { + return Err(Error::msg(format!( + "type mismatch: {self} can not be of type {t}" + ))) } - } + }, _ => { return Err(Error::msg(format!( "type mismatch: {self} cannot be of type {t}" @@ -451,8 +443,7 @@ impl<'de> Visitor<'de> for IDLValueVisitor { 5u8 => { use std::io::Read; let len = leb128::read::unsigned(&mut bytes).map_err(E::custom)? as usize; - let mut buf = Vec::new(); - buf.resize(len, 0); + let mut buf = vec![0; len]; bytes.read_exact(&mut buf).map_err(E::custom)?; let meth = String::from_utf8(buf).map_err(E::custom)?; let id = crate::Principal::try_from(bytes).map_err(E::custom)?; diff --git a/rust/candid/src/utils.rs b/rust/candid/src/utils.rs index 64bee0a2..be57f900 100644 --- a/rust/candid/src/utils.rs +++ b/rust/candid/src/utils.rs @@ -3,13 +3,6 @@ use crate::ser::IDLBuilder; use crate::{CandidType, Error, Result}; use serde::de::Deserialize; -#[cfg(feature = "parser")] -use crate::{check_prog, pretty_check_file}; -#[cfg(feature = "parser")] -use crate::{pretty_parse, types::Type, TypeEnv}; -#[cfg(feature = "parser")] -use std::path::Path; - pub fn check_unique<'a, I, T>(sorted: I) -> Result<()> where T: 'a + PartialEq + std::fmt::Display, @@ -29,96 +22,6 @@ where Ok(()) } -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub enum CandidSource<'a> { - File(&'a Path), - Text(&'a str), -} -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -impl<'a> CandidSource<'a> { - pub fn load(&self) -> Result<(TypeEnv, Option)> { - Ok(match self { - CandidSource::File(path) => pretty_check_file(path)?, - CandidSource::Text(str) => { - let ast = pretty_parse("", str)?; - let mut env = TypeEnv::new(); - let actor = check_prog(&mut env, &ast)?; - (env, actor) - } - }) - } -} - -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -/// Check compatibility of two service types -pub fn service_compatible(new: CandidSource, old: CandidSource) -> Result<()> { - let (mut env, t1) = new.load()?; - let t1 = t1.ok_or_else(|| Error::msg("new interface has no main service type"))?; - let (env2, t2) = old.load()?; - let t2 = t2.ok_or_else(|| Error::msg("old interface has no main service type"))?; - let mut gamma = std::collections::HashSet::new(); - let t2 = env.merge_type(env2, t2); - crate::types::subtype::subtype(&mut gamma, &env, &t1, &t2)?; - Ok(()) -} - -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -/// Check structural equality of two service types -pub fn service_equal(left: CandidSource, right: CandidSource) -> Result<()> { - let (mut env, t1) = left.load()?; - let t1 = t1.ok_or_else(|| Error::msg("left interface has no main service type"))?; - let (env2, t2) = right.load()?; - let t2 = t2.ok_or_else(|| Error::msg("right interface has no main service type"))?; - let mut gamma = std::collections::HashSet::new(); - let t2 = env.merge_type(env2, t2); - crate::types::subtype::equal(&mut gamma, &env, &t1, &t2)?; - Ok(()) -} - -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -/// Take a did file and outputs the init args and the service type (without init args). -/// If the original did file contains imports, the output flattens the type definitions. -/// For now, the comments from the original did file is omitted. -pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, Type))> { - use crate::types::TypeInner; - let (env, serv) = candid.load()?; - let serv = serv.ok_or_else(|| Error::msg("the Candid interface has no main service type"))?; - let serv = env.trace_type(&serv)?; - Ok(match serv.as_ref() { - TypeInner::Class(args, ty) => (args.clone(), (env, ty.clone())), - TypeInner::Service(_) => (vec![], (env, serv)), - _ => unreachable!(), - }) -} - -/// Merge canister metadata candid:args and candid:service into a service constructor. -/// If candid:service already contains init args, returns the original did file. -#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] -#[cfg(feature = "parser")] -pub fn merge_init_args(candid: &str, init: &str) -> Result<(TypeEnv, Type)> { - use crate::parser::{types::IDLInitArgs, typing::check_init_args}; - use crate::types::TypeInner; - let candid = CandidSource::Text(candid); - let (env, serv) = candid.load()?; - let serv = serv.ok_or_else(|| Error::msg("the Candid interface has no main service type"))?; - let serv = env.trace_type(&serv)?; - match serv.as_ref() { - TypeInner::Class(_, _) => Ok((env, serv)), - TypeInner::Service(_) => { - let prog = init.parse::()?; - let mut env2 = TypeEnv::new(); - let args = check_init_args(&mut env2, &env, &prog)?; - Ok((env2, TypeInner::Class(args, serv).into())) - } - _ => unreachable!(), - } -} - /// Encode sequence of Rust values into Candid message of type `candid::Result>`. #[macro_export] macro_rules! Encode { diff --git a/rust/candid_derive/src/func.rs b/rust/candid_derive/src/func.rs index b88afb22..c060d76f 100644 --- a/rust/candid_derive/src/func.rs +++ b/rust/candid_derive/src/func.rs @@ -146,7 +146,7 @@ pub(crate) fn export_service(path: Option) -> TokenStream { fn __export_service() -> String { #service #actor - let result = #candid::bindings::candid::compile(&env.env, &actor); + let result = #candid::pretty_printer::compile(&env.env, &actor); format!("{}", result) } }; diff --git a/rust/candid_parser/Cargo.toml b/rust/candid_parser/Cargo.toml new file mode 100644 index 00000000..eac40b32 --- /dev/null +++ b/rust/candid_parser/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "candid_parser" +version = "0.1.0" +edition = "2021" +authors = ["DFINITY Team"] +description = "Candid is an interface description language (IDL) for interacting with canisters running on the Internet Computer." +homepage = "https://internetcomputer.org/docs/current/developer-docs/build/candid/candid-concepts" +documentation = "https://docs.rs/candid_parser" +repository = "https://github.com/dfinity/candid" +license = "Apache-2.0" +readme = "README.md" +categories = ["encoding", "parsing", "wasm"] +keywords = ["internet-computer", "idl", "candid", "dfinity", "parser"] +include = ["src", "Cargo.toml", "build.rs", "LICENSE", "README.md"] +build = "build.rs" + +[build-dependencies] +lalrpop = "0.20.0" + +[dependencies] +candid = { path = "../candid", version = "0.10" } +codespan-reporting = "0.11" +hex = "0.4.2" +num-bigint = { version = "0.4.2", features = ["serde"] } +num-traits = "0.2.12" +pretty = "0.12.0" +serde = { version = "1.0.118", features = ["derive"] } +serde_bytes = "0.11" +sha2 = "0.10.1" +thiserror = "1.0.20" +anyhow = "1.0" + +lalrpop-util = "0.20.0" +logos = "0.13" +convert_case = "0.6" + +arbitrary = { version = "1.0", optional = true } +# Don't upgrade serde_dhall. It will introduce dependency with invalid license. +serde_dhall = { version = "0.11", default-features = false, optional = true } +fake = { version = "2.4", optional = true } +rand = { version = "0.8", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +stacker = "0.1" + +[dev-dependencies] +goldenfile = "1.1.0" +test-generator = "0.3.0" +rand = "0.8" + +[features] +configs = ["serde_dhall"] +random = ["configs", "arbitrary", "fake", "rand"] +all = ["random"] + +# docs.rs-specific configuration +# To test locally: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features all +[package.metadata.docs.rs] +features = ["all"] +# defines the configuration attribute `docsrs` +rustdoc-args = ["--cfg", "docsrs"] diff --git a/rust/candid_parser/LICENSE b/rust/candid_parser/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/rust/candid_parser/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/rust/candid/build.rs b/rust/candid_parser/build.rs similarity index 62% rename from rust/candid/build.rs rename to rust/candid_parser/build.rs index e6396a04..d8dc3bac 100644 --- a/rust/candid/build.rs +++ b/rust/candid_parser/build.rs @@ -1,8 +1,7 @@ fn main() { - #[cfg(feature = "parser")] lalrpop::Configuration::new() .use_cargo_dir_conventions() .emit_rerun_directives(true) - .process_file("src/parser/grammar.lalrpop") + .process_file("src/grammar.lalrpop") .unwrap(); } diff --git a/rust/candid/src/bindings/analysis.rs b/rust/candid_parser/src/bindings/analysis.rs similarity index 98% rename from rust/candid/src/bindings/analysis.rs rename to rust/candid_parser/src/bindings/analysis.rs index a6cc6d22..6d8f717c 100644 --- a/rust/candid/src/bindings/analysis.rs +++ b/rust/candid_parser/src/bindings/analysis.rs @@ -1,5 +1,5 @@ -use crate::types::{Type, TypeEnv, TypeInner}; use crate::Result; +use candid::types::{Type, TypeEnv, TypeInner}; use std::collections::BTreeSet; /// Same as chase_actor, with seen set as part of the type. Used for chasing type names from type definitions. diff --git a/rust/candid/src/bindings/javascript.rs b/rust/candid_parser/src/bindings/javascript.rs similarity index 95% rename from rust/candid/src/bindings/javascript.rs rename to rust/candid_parser/src/bindings/javascript.rs index 739fba25..6b3ecf74 100644 --- a/rust/candid/src/bindings/javascript.rs +++ b/rust/candid_parser/src/bindings/javascript.rs @@ -1,6 +1,6 @@ use super::analysis::{chase_actor, chase_types, infer_rec}; -use crate::pretty::*; -use crate::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; +use candid::pretty::*; +use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use pretty::RcDoc; use std::collections::BTreeSet; @@ -170,7 +170,7 @@ fn pp_args(args: &[Type]) -> RcDoc { enclose("[", doc, "]") } -fn pp_modes(modes: &[crate::types::FuncMode]) -> RcDoc { +fn pp_modes(modes: &[candid::types::FuncMode]) -> RcDoc { let doc = concat( modes .iter() @@ -267,10 +267,10 @@ pub fn compile(env: &TypeEnv, actor: &Option) -> String { } pub mod value { - use super::super::candid::value::number_to_string; - use crate::pretty::*; - use crate::types::value::{IDLArgs, IDLField, IDLValue}; - use crate::types::Label; + use candid::pretty::*; + use candid::pretty_printer::value::number_to_string; + use candid::types::value::{IDLArgs, IDLField, IDLValue}; + use candid::types::Label; use pretty::RcDoc; fn is_tuple(v: &IDLValue) -> bool { @@ -357,9 +357,9 @@ pub mod value { pub mod test { use super::value; - use crate::parser::test::{HostAssert, HostTest, Test}; - use crate::pretty::*; - use crate::TypeEnv; + use crate::test::{HostAssert, HostTest, Test}; + use candid::pretty::*; + use candid::TypeEnv; use pretty::RcDoc; fn pp_hex(bytes: &[u8]) -> RcDoc { @@ -367,7 +367,7 @@ pub mod test { .append(RcDoc::as_string(hex::encode(bytes))) .append("', 'hex')") } - fn pp_encode<'a>(args: &'a crate::IDLArgs, tys: &'a [crate::types::Type]) -> RcDoc<'a> { + fn pp_encode<'a>(args: &'a candid::IDLArgs, tys: &'a [candid::types::Type]) -> RcDoc<'a> { let vals = value::pp_args(args); let tys = super::pp_args(tys); let items = [tys, vals]; @@ -375,7 +375,7 @@ pub mod test { str("IDL.encode").append(enclose("(", params, ")")) } - fn pp_decode<'a>(bytes: &'a [u8], tys: &'a [crate::types::Type]) -> RcDoc<'a> { + fn pp_decode<'a>(bytes: &'a [u8], tys: &'a [candid::types::Type]) -> RcDoc<'a> { let hex = pp_hex(bytes); let tys = super::pp_args(tys); let items = [tys, hex]; @@ -404,7 +404,7 @@ import { Principal } from './principal'; for (i, assert) in test.asserts.iter().enumerate() { let mut types = Vec::new(); for ty in assert.typ.iter() { - types.push(env.ast_to_type(ty).unwrap()); + types.push(crate::typing::ast_to_type(&env, ty).unwrap()); } let host = HostTest::from_assert(assert, &env, &types); let mut expects = Vec::new(); diff --git a/rust/candid_parser/src/bindings/mod.rs b/rust/candid_parser/src/bindings/mod.rs new file mode 100644 index 00000000..ae2a95b4 --- /dev/null +++ b/rust/candid_parser/src/bindings/mod.rs @@ -0,0 +1,8 @@ +//! Candid bindings for different languages. +// This module assumes the input are type checked, it is safe to use unwrap. + +pub mod analysis; +pub mod javascript; +pub mod motoko; +pub mod rust; +pub mod typescript; diff --git a/rust/candid/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs similarity index 95% rename from rust/candid/src/bindings/motoko.rs rename to rust/candid_parser/src/bindings/motoko.rs index 48148e68..ef4079b5 100644 --- a/rust/candid/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -1,10 +1,10 @@ // This module implements the Candid to Motoko binding as specified in // https://github.com/dfinity/motoko/blob/master/design/IDL-Motoko.md -use super::candid::is_valid_as_id; -use crate::pretty::*; -use crate::types::FuncMode; -use crate::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; +use candid::pretty::*; +use candid::pretty_printer::is_valid_as_id; +use candid::types::FuncMode; +use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use pretty::RcDoc; // The definition of tuple is language specific. @@ -84,7 +84,9 @@ fn escape(id: &str, is_method: bool) -> RcDoc { str(id) } } else if !is_method { - str("_").append(crate::idl_hash(id).to_string()).append("_") + str("_") + .append(candid::idl_hash(id).to_string()) + .append("_") } else { panic!("Candid method {id} is not a valid Motoko id"); } diff --git a/rust/candid/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs similarity index 98% rename from rust/candid/src/bindings/rust.rs rename to rust/candid_parser/src/bindings/rust.rs index fe399dbb..256d2ef8 100644 --- a/rust/candid/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -1,6 +1,6 @@ use super::analysis::{chase_actor, infer_rec}; -use crate::pretty::*; -use crate::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; +use candid::pretty::*; +use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use convert_case::{Case, Casing}; use pretty::RcDoc; use std::collections::BTreeSet; @@ -18,7 +18,7 @@ pub struct Config { /// Applies to all types for now pub type_attributes: String, /// Only generates SERVICE struct if canister_id is not provided - pub canister_id: Option, + pub canister_id: Option, /// Service name when canister id is provided pub service_name: String, pub target: Target, @@ -62,7 +62,7 @@ fn ident_(id: &str, case: Option) -> (RcDoc, bool) { || id.starts_with(|c: char| !c.is_ascii_alphabetic() && c != '_') || id.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') { - return (RcDoc::text(format!("_{}_", crate::idl_hash(id))), true); + return (RcDoc::text(format!("_{}_", candid::idl_hash(id))), true); } let (is_rename, id) = if let Some(case) = case { let new_id = id.to_case(case); @@ -269,7 +269,7 @@ fn pp_args(args: &[Type]) -> RcDoc { fn pp_ty_func(f: &Function) -> RcDoc { let args = pp_args(&f.args); let rets = pp_args(&f.rets); - let modes = super::candid::pp_modes(&f.modes); + let modes = candid::pretty_printer::pp_modes(&f.modes); args.append(" ->") .append(RcDoc::space()) .append(rets.append(modes)) diff --git a/rust/candid/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs similarity index 98% rename from rust/candid/src/bindings/typescript.rs rename to rust/candid_parser/src/bindings/typescript.rs index 9d9bc783..ddb4451b 100644 --- a/rust/candid/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -1,6 +1,6 @@ use super::javascript::{ident, is_tuple}; -use crate::pretty::*; -use crate::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; +use candid::pretty::*; +use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use pretty::RcDoc; fn pp_ty<'a>(env: &'a TypeEnv, ty: &'a Type, is_ref: bool) -> RcDoc<'a> { diff --git a/rust/candid/src/parser/configs.rs b/rust/candid_parser/src/configs.rs similarity index 99% rename from rust/candid/src/parser/configs.rs rename to rust/candid_parser/src/configs.rs index 39371c86..7acceb41 100644 --- a/rust/candid/src/parser/configs.rs +++ b/rust/candid_parser/src/configs.rs @@ -1,5 +1,5 @@ -use crate::types::Type; use crate::Result; +use candid::types::Type; use serde::de::DeserializeOwned; use serde_dhall::{from_simple_value, SimpleValue}; diff --git a/rust/candid_parser/src/error.rs b/rust/candid_parser/src/error.rs new file mode 100644 index 00000000..db8e86d3 --- /dev/null +++ b/rust/candid_parser/src/error.rs @@ -0,0 +1,126 @@ +//! When serializing or deserializing Candid goes wrong. + +use codespan_reporting::diagnostic::Label; +use std::io; +use thiserror::Error; + +use crate::token; +use codespan_reporting::{ + diagnostic::Diagnostic, + files::{Error as ReportError, SimpleFile}, + term::{self, termcolor::StandardStream}, +}; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Candid parser error: {0}")] + Parse(#[from] token::ParserError), + + #[error(transparent)] + Custom(#[from] anyhow::Error), + + #[error(transparent)] + CandidError(#[from] ::candid::Error), +} + +impl Error { + pub fn msg(msg: T) -> Self { + Error::Custom(anyhow::anyhow!(msg.to_string())) + } + + pub fn report(&self) -> Diagnostic<()> { + match self { + Error::Parse(e) => { + use lalrpop_util::ParseError::*; + let mut diag = Diagnostic::error().with_message("parser error"); + let label = match e { + User { error } => { + Label::primary((), error.span.clone()).with_message(&error.err) + } + InvalidToken { location } => { + Label::primary((), *location..location + 1).with_message("Invalid token") + } + UnrecognizedEof { location, expected } => { + diag = diag.with_notes(report_expected(expected)); + Label::primary((), *location..location + 1).with_message("Unexpected EOF") + } + UnrecognizedToken { token, expected } => { + diag = diag.with_notes(report_expected(expected)); + Label::primary((), token.0..token.2).with_message("Unexpected token") + } + ExtraToken { token } => { + Label::primary((), token.0..token.2).with_message("Extra token") + } + }; + diag.with_labels(vec![label]) + } + Error::Custom(e) => Diagnostic::error().with_message(e.to_string()), + Error::CandidError(e) => Diagnostic::error().with_message(e.to_string()), + } + } +} + +fn report_expected(expected: &[String]) -> Vec { + if expected.is_empty() { + return Vec::new(); + } + use pretty::RcDoc; + let doc: RcDoc<()> = RcDoc::intersperse( + expected.iter().map(RcDoc::text), + RcDoc::text(",").append(RcDoc::softline()), + ); + let header = if expected.len() == 1 { + "Expects" + } else { + "Expects one of" + }; + let doc = RcDoc::text(header).append(RcDoc::softline().append(doc)); + vec![doc.pretty(70).to_string()] +} + +impl From for Error { + fn from(e: io::Error) -> Error { + Error::msg(format!("io error: {e}")) + } +} + +impl From for Error { + fn from(e: ReportError) -> Error { + Error::msg(e) + } +} +#[cfg_attr(docsrs, doc(cfg(feature = "random")))] +#[cfg(feature = "random")] +impl From for Error { + fn from(e: arbitrary::Error) -> Error { + Error::msg(format!("arbitrary error: {e}")) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "configs")))] +#[cfg(feature = "configs")] +impl From for Error { + fn from(e: serde_dhall::Error) -> Error { + Error::msg(format!("dhall error: {e}")) + } +} + +pub fn pretty_parse(name: &str, str: &str) -> Result +where + T: std::str::FromStr, +{ + str.parse::().or_else(|e| { + pretty_diagnose(name, str, &e)?; + Err(e) + }) +} + +pub fn pretty_diagnose(file_name: &str, source: &str, e: &Error) -> Result<()> { + let writer = StandardStream::stderr(term::termcolor::ColorChoice::Auto); + let config = term::Config::default(); + let file = SimpleFile::new(file_name, source); + term::emit(&mut writer.lock(), &config, &file, &e.report())?; + Ok(()) +} diff --git a/rust/candid/src/parser/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop similarity index 97% rename from rust/candid/src/parser/grammar.lalrpop rename to rust/candid_parser/src/grammar.lalrpop index 6f0e2356..153924f0 100644 --- a/rust/candid/src/parser/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,10 +1,10 @@ -use crate::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; -use crate::types::{TypeEnv, FuncMode}; -use crate::utils::check_unique; use super::types::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs}; use super::test::{Assert, Input, Test}; use super::token::{Token, error2, LexicalError, Span}; -use crate::{Principal, types::Label}; +use candid::{Principal, types::Label}; +use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; +use candid::types::{TypeEnv, FuncMode}; +use candid::utils::check_unique; grammar; @@ -116,7 +116,7 @@ AnnVal: IDLValue = { => <>, > ":" > =>? { let env = TypeEnv::new(); - let typ = env.ast_to_type(&typ.0).map_err(|e| error2(e, typ.1))?; + let typ = crate::typing::ast_to_type(&env, &typ.0).map_err(|e| error2(e, typ.1))?; arg.0.annotate_type(true, &env, &typ).map_err(|e| error2(e, arg.1)) } } diff --git a/rust/candid_parser/src/grammar.rs b/rust/candid_parser/src/grammar.rs new file mode 100644 index 00000000..8fa4f72f --- /dev/null +++ b/rust/candid_parser/src/grammar.rs @@ -0,0 +1,2 @@ +#![allow(clippy::all)] +include!(concat!(env!("OUT_DIR"), "/grammar.rs")); diff --git a/rust/candid_parser/src/lib.rs b/rust/candid_parser/src/lib.rs new file mode 100644 index 00000000..1cbc0e46 --- /dev/null +++ b/rust/candid_parser/src/lib.rs @@ -0,0 +1,147 @@ +//! # Candid Parser +//! +//! Provides parser for Candid type and value. +//! * `str.parse::()` parses the Candid signature file to Candid AST. +//! * `parse_idl_args()` parses the Candid value in text format to a struct `IDLArg` that can be used for serialization and deserialization between Candid and an enum type `IDLValue` in Rust. + +//! ## Parse [`candid::IDLArgs`] +//! +//! We provide a parser to parse Candid values in text format. +//! +//! ``` +//! # fn f() -> anyhow::Result<()> { +//! use candid::{IDLArgs, TypeEnv}; +//! use candid_parser::parse_idl_args; +//! // Candid values represented in text format +//! let text_value = r#" +//! (42, opt true, vec {1;2;3}, +//! opt record {label="text"; 42="haha"}) +//! "#; +//! +//! // Parse text format into IDLArgs for serialization +//! let args: IDLArgs = parse_idl_args(text_value)?; +//! let encoded: Vec = args.to_bytes()?; +//! +//! // Deserialize into IDLArgs +//! let decoded: IDLArgs = IDLArgs::from_bytes(&encoded)?; +//! assert_eq!(encoded, decoded.to_bytes()?); +//! +//! // Convert IDLArgs to text format +//! let output: String = decoded.to_string(); +//! let parsed_args: IDLArgs = parse_idl_args(&output)?; +//! let annotated_args = args.annotate_types(true, &TypeEnv::new(), &parsed_args.get_types())?; +//! assert_eq!(annotated_args, parsed_args); +//! # Ok(()) +//! # } +//! ``` +//! Note that when parsing Candid values, we assume the number literals are always of type `Int`. +//! This can be changed by providing the type of the method arguments, which can usually be obtained +//! by parsing a Candid file in the following section. +//! +//! ## Operating on Candid AST +//! +//! We provide a parser and type checker for Candid files specifying the service interface. +//! +//! ``` +//! # fn f() -> anyhow::Result<()> { +//! use candid::{TypeEnv, types::{Type, TypeInner}}; +//! use candid_parser::{IDLProg, check_prog}; +//! let did_file = r#" +//! type List = opt record { head: int; tail: List }; +//! type byte = nat8; +//! service : { +//! f : (byte, int, nat, int8) -> (List); +//! g : (List) -> (int) query; +//! } +//! "#; +//! +//! // Parse did file into an AST +//! let ast: IDLProg = did_file.parse()?; +//! +//! // Type checking a given .did file +//! // let (env, opt_actor) = check_file("a.did")?; +//! // Or alternatively, use check_prog to check in-memory did file +//! // Note that file import is ignored by check_prog. +//! let mut env = TypeEnv::new(); +//! let actor: Type = check_prog(&mut env, &ast)?.unwrap(); +//! +//! let method = env.get_method(&actor, "g").unwrap(); +//! assert_eq!(method.is_query(), true); +//! assert_eq!(method.args, vec![TypeInner::Var("List".to_string()).into()]); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Serializing untyped Candid values with type annotations. +//! +//! With type signatures from the Candid file, [`candid::IDLArgs`](parser/value/struct.IDLArgs.html) +//! uses `to_bytes_with_types` function to serialize arguments directed by the Candid types. +//! This is useful when serializing different number types and recursive types. +//! There is no need to use types for deserialization as the types are available in the Candid message. +//! +//! ``` +//! # fn f() -> anyhow::Result<()> { +//! use candid::{IDLArgs, types::value::IDLValue}; +//! use candid_parser::parse_idl_args; +//! # use candid::TypeEnv; +//! # use candid_parser::{IDLProg, check_prog}; +//! # let did_file = r#" +//! # type List = opt record { head: int; tail: List }; +//! # type byte = nat8; +//! # service : { +//! # f : (byte, int, nat, int8) -> (List); +//! # g : (List) -> (int) query; +//! # } +//! # "#; +//! # let ast = did_file.parse::()?; +//! # let mut env = TypeEnv::new(); +//! # let actor = check_prog(&mut env, &ast)?.unwrap(); +//! // Get method type f : (byte, int, nat, int8) -> (List) +//! let method = env.get_method(&actor, "f").unwrap(); +//! let args = parse_idl_args("(42, 42, 42, 42)")?; +//! // Serialize arguments with candid types +//! let encoded = args.to_bytes_with_types(&env, &method.args)?; +//! let decoded = IDLArgs::from_bytes(&encoded)?; +//! assert_eq!(decoded.args, +//! vec![IDLValue::Nat8(42), +//! IDLValue::Int(42.into()), +//! IDLValue::Nat(42.into()), +//! IDLValue::Int8(42) +//! ]); +//! # Ok(()) +//! # } +//! ``` + +// only enables the `doc_cfg` feature when +// the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] + +pub mod error; +pub use error::{pretty_parse, Error, Result}; + +pub mod bindings; +pub mod grammar; +pub mod token; +pub mod types; +pub mod utils; +pub use types::IDLProg; +pub mod typing; +pub use typing::{check_file, check_prog, pretty_check_file}; + +#[cfg_attr(docsrs, doc(cfg(feature = "configs")))] +#[cfg(feature = "configs")] +pub mod configs; +#[cfg_attr(docsrs, doc(cfg(feature = "random")))] +#[cfg(feature = "random")] +pub mod random; +pub mod test; + +pub fn parse_idl_args(s: &str) -> crate::Result { + let lexer = token::Tokenizer::new(s); + Ok(grammar::ArgsParser::new().parse(lexer)?) +} + +pub fn parse_idl_value(s: &str) -> crate::Result { + let lexer = token::Tokenizer::new(s); + Ok(grammar::ArgParser::new().parse(lexer)?) +} diff --git a/rust/candid/src/parser/random.rs b/rust/candid_parser/src/random.rs similarity index 82% rename from rust/candid/src/parser/random.rs rename to rust/candid_parser/src/random.rs index 79ce9019..b35fe6a1 100644 --- a/rust/candid/src/parser/random.rs +++ b/rust/candid_parser/src/random.rs @@ -1,9 +1,9 @@ use super::configs::{path_name, Configs}; -use crate::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; -use crate::types::{Field, Type, TypeEnv, TypeInner}; -use crate::Deserialize; use crate::{Error, Result}; use arbitrary::{unstructured::Int, Arbitrary, Unstructured}; +use candid::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; +use candid::types::{Field, Type, TypeEnv, TypeInner}; +use candid::Deserialize; use std::collections::HashSet; use std::convert::TryFrom; @@ -115,7 +115,7 @@ impl<'a> GenState<'a> { assert!(self.config.depth.is_none()); if let Some(vec) = &self.config.value { let v = u.choose(vec)?; - let v = v.parse::()?; + let v: IDLValue = super::parse_idl_value(v)?; let v = v.annotate_type(true, self.env, ty)?; self.pop_state(old_config, ty, false); return Ok(v); @@ -140,7 +140,7 @@ impl<'a> GenState<'a> { TypeInner::Int64 => IDLValue::Int64(arbitrary_num(u, self.config.range)?), TypeInner::Float32 => IDLValue::Float32(u.arbitrary()?), TypeInner::Float64 => IDLValue::Float64(u.arbitrary()?), - TypeInner::Principal => IDLValue::Principal(crate::Principal::anonymous()), + TypeInner::Principal => IDLValue::Principal(candid::Principal::anonymous()), TypeInner::Text => { IDLValue::Text(arbitrary_text(u, &self.config.text, &self.config.width)?) } @@ -148,7 +148,7 @@ impl<'a> GenState<'a> { let depths = if self.depth <= 0 || self.size <= 0 { [1, 0] } else { - [1, self.env.size(t).unwrap_or(MAX_DEPTH)] + [1, size(self.env, t).unwrap_or(MAX_DEPTH)] }; let idx = arbitrary_variant(u, &depths)?; if idx == 0 { @@ -159,7 +159,7 @@ impl<'a> GenState<'a> { } TypeInner::Vec(t) => { let width = self.config.width.or_else(|| { - let elem_size = self.env.size(t).unwrap_or(MAX_DEPTH); + let elem_size = size(self.env, t).unwrap_or(MAX_DEPTH); Some(std::cmp::max(0, self.size) as usize / elem_size) }); let len = arbitrary_len(u, width)?; @@ -186,7 +186,7 @@ impl<'a> GenState<'a> { TypeInner::Variant(fs) => { let choices = fs .iter() - .map(|Field { ty, .. }| self.env.size(ty).unwrap_or(MAX_DEPTH)); + .map(|Field { ty, .. }| size(self.env, ty).unwrap_or(MAX_DEPTH)); let sizes: Vec<_> = if self.depth <= 0 || self.size <= 0 { let min = choices.clone().min().unwrap_or(0); choices.map(|d| if d > min { 0 } else { d }).collect() @@ -211,62 +211,58 @@ impl<'a> GenState<'a> { } } -impl IDLArgs { - pub fn any(seed: &[u8], tree: &Configs, env: &TypeEnv, types: &[Type]) -> Result { - let mut u = arbitrary::Unstructured::new(seed); - let mut args = Vec::new(); - for (i, t) in types.iter().enumerate() { - let tree = tree.with_method(&i.to_string()); - let mut state = GenState::new(&tree, env); - let v = state.any(&mut u, t)?; - args.push(v); - } - Ok(IDLArgs { args }) +pub fn any(seed: &[u8], tree: &Configs, env: &TypeEnv, types: &[Type]) -> Result { + let mut u = arbitrary::Unstructured::new(seed); + let mut args = Vec::new(); + for (i, t) in types.iter().enumerate() { + let tree = tree.with_method(&i.to_string()); + let mut state = GenState::new(&tree, env); + let v = state.any(&mut u, t)?; + args.push(v); } + Ok(IDLArgs { args }) } -impl TypeEnv { - /// Approxiamte upper bound for IDLValue size of type t. Returns None if infinite. - fn size_helper(&self, seen: &mut HashSet, t: &Type) -> Option { - use TypeInner::*; - Some(match t.as_ref() { - Var(id) => { - if seen.insert(id.to_string()) { - let ty = self.rec_find_type(id).unwrap(); - let res = self.size_helper(seen, ty)?; - seen.remove(id); - res - } else { - return None; - } +fn size_helper(env: &TypeEnv, seen: &mut HashSet, t: &Type) -> Option { + use TypeInner::*; + Some(match t.as_ref() { + Var(id) => { + if seen.insert(id.to_string()) { + let ty = env.rec_find_type(id).unwrap(); + let res = size_helper(env, seen, ty)?; + seen.remove(id); + res + } else { + return None; } - Empty => 0, - Opt(t) => 1 + self.size_helper(seen, t)?, - Vec(t) => 1 + self.size_helper(seen, t)? * 2, - Record(fs) => { - let mut sum = 0; - for Field { ty, .. } in fs.iter() { - sum += self.size_helper(seen, ty)?; - } - 1 + sum + } + Empty => 0, + Opt(t) => 1 + size_helper(env, seen, t)?, + Vec(t) => 1 + size_helper(env, seen, t)? * 2, + Record(fs) => { + let mut sum = 0; + for Field { ty, .. } in fs.iter() { + sum += size_helper(env, seen, ty)?; } - Variant(fs) => { - let mut max = 0; - for Field { ty, .. } in fs.iter() { - let s = self.size_helper(seen, ty)?; - if s > max { - max = s; - }; - } - 1 + max + 1 + sum + } + Variant(fs) => { + let mut max = 0; + for Field { ty, .. } in fs.iter() { + let s = size_helper(env, seen, ty)?; + if s > max { + max = s; + }; } - _ => 1, - }) - } - fn size(&self, t: &Type) -> Option { - let mut seen = HashSet::new(); - self.size_helper(&mut seen, t) - } + 1 + max + } + _ => 1, + }) +} + +fn size(env: &TypeEnv, t: &Type) -> Option { + let mut seen = HashSet::new(); + size_helper(env, &mut seen, t) } fn choose_range(u: &mut Unstructured, ranges: &[std::ops::RangeInclusive]) -> Result { diff --git a/rust/candid/src/parser/test.rs b/rust/candid_parser/src/test.rs similarity index 95% rename from rust/candid/src/parser/test.rs rename to rust/candid_parser/src/test.rs index 7fefd6d1..f48c2ac4 100644 --- a/rust/candid/src/parser/test.rs +++ b/rust/candid_parser/src/test.rs @@ -1,8 +1,8 @@ use super::types::{Dec, IDLProg, IDLType}; use super::typing::check_prog; -use crate::types::value::IDLArgs; -use crate::types::{Type, TypeEnv}; use crate::{Error, Result}; +use candid::types::value::IDLArgs; +use candid::types::{Type, TypeEnv}; type TupType = Vec; @@ -50,7 +50,7 @@ impl Assert { impl Input { pub fn parse(&self, env: &TypeEnv, types: &[Type]) -> Result { match self { - Input::Text(ref s) => s.parse::()?.annotate_types(true, env, types), + Input::Text(ref s) => Ok(super::parse_idl_args(s)?.annotate_types(true, env, types)?), Input::Blob(ref bytes) => Ok(IDLArgs::from_bytes_with_types(bytes, env, types)?), } } @@ -82,7 +82,7 @@ impl HostTest { Input::Text(s) => { // Without type annotation, numbers are all of type int. // Assertion may not pass. - let parsed = s.parse::(); + let parsed = crate::parse_idl_args(s); if parsed.is_err() { let desc = format!("(skip) {}", assert.desc()); return HostTest { desc, asserts }; @@ -145,7 +145,7 @@ pub fn check(test: Test) -> Result<()> { print!("Checking {} {}...", i + 1, assert.desc()); let mut types = Vec::new(); for ty in assert.typ.iter() { - types.push(env.ast_to_type(ty)?); + types.push(super::typing::ast_to_type(&env, ty)?); } let input = assert.left.parse(&env, &types); let pass = if let Some(assert_right) = &assert.right { diff --git a/rust/candid/src/parser/token.rs b/rust/candid_parser/src/token.rs similarity index 100% rename from rust/candid/src/parser/token.rs rename to rust/candid_parser/src/token.rs diff --git a/rust/candid/src/parser/types.rs b/rust/candid_parser/src/types.rs similarity index 98% rename from rust/candid/src/parser/types.rs rename to rust/candid_parser/src/types.rs index 5c9794f6..70861704 100644 --- a/rust/candid/src/parser/types.rs +++ b/rust/candid_parser/src/types.rs @@ -1,5 +1,5 @@ -use crate::types::{FuncMode, Label}; use crate::Result; +use candid::types::{FuncMode, Label}; #[derive(Debug, Clone)] pub enum IDLType { diff --git a/rust/candid/src/parser/typing.rs b/rust/candid_parser/src/typing.rs similarity index 95% rename from rust/candid/src/parser/typing.rs rename to rust/candid_parser/src/typing.rs index 1b126d59..60bd26ba 100644 --- a/rust/candid/src/parser/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,6 +1,6 @@ use super::types::*; -use crate::types::{Field, Function, Type, TypeEnv, TypeInner}; use crate::{pretty_parse, Error, Result}; +use candid::types::{Field, Function, Type, TypeEnv, TypeInner}; use std::collections::BTreeSet; use std::path::{Path, PathBuf}; @@ -9,15 +9,13 @@ pub struct Env<'a> { pub pre: bool, } -impl TypeEnv { - /// Convert candid AST to internal Type - pub fn ast_to_type(&self, ast: &super::types::IDLType) -> Result { - let env = Env { - te: &mut self.clone(), - pre: false, - }; - check_type(&env, ast) - } +/// Convert candid AST to internal Type +pub fn ast_to_type(env: &TypeEnv, ast: &super::types::IDLType) -> Result { + let env = Env { + te: &mut env.clone(), + pre: false, + }; + check_type(&env, ast) } fn check_prim(prim: &PrimType) -> Type { @@ -80,7 +78,7 @@ pub fn check_type(env: &Env, t: &IDLType) -> Result { return Err(Error::msg("cannot have more than one mode")); } if func.modes.len() == 1 - && func.modes[0] == crate::types::FuncMode::Oneway + && func.modes[0] == candid::types::FuncMode::Oneway && !t2.is_empty() { return Err(Error::msg("oneway function has non-unit return type")); diff --git a/rust/candid_parser/src/utils.rs b/rust/candid_parser/src/utils.rs new file mode 100644 index 00000000..82a1b32c --- /dev/null +++ b/rust/candid_parser/src/utils.rs @@ -0,0 +1,82 @@ +use crate::{check_prog, pretty_check_file, pretty_parse, Error, Result}; +use candid::{types::Type, TypeEnv}; +use std::path::Path; + +pub enum CandidSource<'a> { + File(&'a Path), + Text(&'a str), +} + +impl<'a> CandidSource<'a> { + pub fn load(&self) -> Result<(TypeEnv, Option)> { + Ok(match self { + CandidSource::File(path) => pretty_check_file(path)?, + CandidSource::Text(str) => { + let ast = pretty_parse("", str)?; + let mut env = TypeEnv::new(); + let actor = check_prog(&mut env, &ast)?; + (env, actor) + } + }) + } +} + +/// Check compatibility of two service types +pub fn service_compatible(new: CandidSource, old: CandidSource) -> Result<()> { + let (mut env, t1) = new.load()?; + let t1 = t1.ok_or_else(|| Error::msg("new interface has no main service type"))?; + let (env2, t2) = old.load()?; + let t2 = t2.ok_or_else(|| Error::msg("old interface has no main service type"))?; + let mut gamma = std::collections::HashSet::new(); + let t2 = env.merge_type(env2, t2); + candid::types::subtype::subtype(&mut gamma, &env, &t1, &t2)?; + Ok(()) +} + +/// Check structural equality of two service types +pub fn service_equal(left: CandidSource, right: CandidSource) -> Result<()> { + let (mut env, t1) = left.load()?; + let t1 = t1.ok_or_else(|| Error::msg("left interface has no main service type"))?; + let (env2, t2) = right.load()?; + let t2 = t2.ok_or_else(|| Error::msg("right interface has no main service type"))?; + let mut gamma = std::collections::HashSet::new(); + let t2 = env.merge_type(env2, t2); + candid::types::subtype::equal(&mut gamma, &env, &t1, &t2)?; + Ok(()) +} + +/// Take a did file and outputs the init args and the service type (without init args). +/// If the original did file contains imports, the output flattens the type definitions. +/// For now, the comments from the original did file is omitted. +pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, Type))> { + use candid::types::TypeInner; + let (env, serv) = candid.load()?; + let serv = serv.ok_or_else(|| Error::msg("the Candid interface has no main service type"))?; + let serv = env.trace_type(&serv)?; + Ok(match serv.as_ref() { + TypeInner::Class(args, ty) => (args.clone(), (env, ty.clone())), + TypeInner::Service(_) => (vec![], (env, serv)), + _ => unreachable!(), + }) +} + +/// Merge canister metadata candid:args and candid:service into a service constructor. +/// If candid:service already contains init args, returns the original did file. +pub fn merge_init_args(candid: &str, init: &str) -> Result<(TypeEnv, Type)> { + use crate::{types::IDLInitArgs, typing::check_init_args}; + use candid::types::TypeInner; + let candid = CandidSource::Text(candid); + let (env, serv) = candid.load()?; + let serv = serv.ok_or_else(|| Error::msg("the Candid interface has no main service type"))?; + let serv = env.trace_type(&serv)?; + match serv.as_ref() { + TypeInner::Class(_, _) => Ok((env, serv)), + TypeInner::Service(_) => { + let prog = init.parse::()?; + let mut env2 = TypeEnv::new(); + let args = check_init_args(&mut env2, &env, &prog)?; + Ok((env2, TypeInner::Class(args, serv).into())) + } + _ => unreachable!(), + } +} diff --git a/rust/candid/tests/assets/actor.did b/rust/candid_parser/tests/assets/actor.did similarity index 100% rename from rust/candid/tests/assets/actor.did rename to rust/candid_parser/tests/assets/actor.did diff --git a/rust/candid/tests/assets/bad_comment.did b/rust/candid_parser/tests/assets/bad_comment.did similarity index 100% rename from rust/candid/tests/assets/bad_comment.did rename to rust/candid_parser/tests/assets/bad_comment.did diff --git a/rust/candid/tests/assets/bad_import.did b/rust/candid_parser/tests/assets/bad_import.did similarity index 100% rename from rust/candid/tests/assets/bad_import.did rename to rust/candid_parser/tests/assets/bad_import.did diff --git a/rust/candid/tests/assets/class.did b/rust/candid_parser/tests/assets/class.did similarity index 100% rename from rust/candid/tests/assets/class.did rename to rust/candid_parser/tests/assets/class.did diff --git a/rust/candid/tests/assets/codegen/basic/invalid_id.did b/rust/candid_parser/tests/assets/codegen/basic/invalid_id.did similarity index 100% rename from rust/candid/tests/assets/codegen/basic/invalid_id.did rename to rust/candid_parser/tests/assets/codegen/basic/invalid_id.did diff --git a/rust/candid/tests/assets/codegen/basic/invalid_id.rs b/rust/candid_parser/tests/assets/codegen/basic/invalid_id.rs similarity index 100% rename from rust/candid/tests/assets/codegen/basic/invalid_id.rs rename to rust/candid_parser/tests/assets/codegen/basic/invalid_id.rs diff --git a/rust/candid/tests/assets/codegen/basic/prim_types.did b/rust/candid_parser/tests/assets/codegen/basic/prim_types.did similarity index 100% rename from rust/candid/tests/assets/codegen/basic/prim_types.did rename to rust/candid_parser/tests/assets/codegen/basic/prim_types.did diff --git a/rust/candid/tests/assets/codegen/basic/prim_types.rs b/rust/candid_parser/tests/assets/codegen/basic/prim_types.rs similarity index 100% rename from rust/candid/tests/assets/codegen/basic/prim_types.rs rename to rust/candid_parser/tests/assets/codegen/basic/prim_types.rs diff --git a/rust/candid/tests/assets/codegen/basic/recursive.did b/rust/candid_parser/tests/assets/codegen/basic/recursive.did similarity index 100% rename from rust/candid/tests/assets/codegen/basic/recursive.did rename to rust/candid_parser/tests/assets/codegen/basic/recursive.did diff --git a/rust/candid/tests/assets/codegen/basic/recursive.rs b/rust/candid_parser/tests/assets/codegen/basic/recursive.rs similarity index 100% rename from rust/candid/tests/assets/codegen/basic/recursive.rs rename to rust/candid_parser/tests/assets/codegen/basic/recursive.rs diff --git a/rust/candid/tests/assets/codegen/examples/linkedup.did b/rust/candid_parser/tests/assets/codegen/examples/linkedup.did similarity index 100% rename from rust/candid/tests/assets/codegen/examples/linkedup.did rename to rust/candid_parser/tests/assets/codegen/examples/linkedup.did diff --git a/rust/candid/tests/assets/codegen/examples/linkedup.rs b/rust/candid_parser/tests/assets/codegen/examples/linkedup.rs similarity index 100% rename from rust/candid/tests/assets/codegen/examples/linkedup.rs rename to rust/candid_parser/tests/assets/codegen/examples/linkedup.rs diff --git a/rust/candid/tests/assets/collision_fields.did b/rust/candid_parser/tests/assets/collision_fields.did similarity index 100% rename from rust/candid/tests/assets/collision_fields.did rename to rust/candid_parser/tests/assets/collision_fields.did diff --git a/rust/candid/tests/assets/collision_fields2.did b/rust/candid_parser/tests/assets/collision_fields2.did similarity index 100% rename from rust/candid/tests/assets/collision_fields2.did rename to rust/candid_parser/tests/assets/collision_fields2.did diff --git a/rust/candid/tests/assets/comment.did b/rust/candid_parser/tests/assets/comment.did similarity index 100% rename from rust/candid/tests/assets/comment.did rename to rust/candid_parser/tests/assets/comment.did diff --git a/rust/candid/tests/assets/cyclic.did b/rust/candid_parser/tests/assets/cyclic.did similarity index 100% rename from rust/candid/tests/assets/cyclic.did rename to rust/candid_parser/tests/assets/cyclic.did diff --git a/rust/candid/tests/assets/escape.did b/rust/candid_parser/tests/assets/escape.did similarity index 100% rename from rust/candid/tests/assets/escape.did rename to rust/candid_parser/tests/assets/escape.did diff --git a/rust/candid/tests/assets/example.did b/rust/candid_parser/tests/assets/example.did similarity index 100% rename from rust/candid/tests/assets/example.did rename to rust/candid_parser/tests/assets/example.did diff --git a/rust/candid/tests/assets/fieldnat.did b/rust/candid_parser/tests/assets/fieldnat.did similarity index 100% rename from rust/candid/tests/assets/fieldnat.did rename to rust/candid_parser/tests/assets/fieldnat.did diff --git a/rust/candid/tests/assets/import/a.did b/rust/candid_parser/tests/assets/import/a.did similarity index 100% rename from rust/candid/tests/assets/import/a.did rename to rust/candid_parser/tests/assets/import/a.did diff --git a/rust/candid/tests/assets/import/b/b.did b/rust/candid_parser/tests/assets/import/b/b.did similarity index 100% rename from rust/candid/tests/assets/import/b/b.did rename to rust/candid_parser/tests/assets/import/b/b.did diff --git a/rust/candid/tests/assets/invalid_cyclic.did b/rust/candid_parser/tests/assets/invalid_cyclic.did similarity index 100% rename from rust/candid/tests/assets/invalid_cyclic.did rename to rust/candid_parser/tests/assets/invalid_cyclic.did diff --git a/rust/candid/tests/assets/keyword.did b/rust/candid_parser/tests/assets/keyword.did similarity index 100% rename from rust/candid/tests/assets/keyword.did rename to rust/candid_parser/tests/assets/keyword.did diff --git a/rust/candid/tests/assets/management.did b/rust/candid_parser/tests/assets/management.did similarity index 100% rename from rust/candid/tests/assets/management.did rename to rust/candid_parser/tests/assets/management.did diff --git a/rust/candid/tests/assets/not_func.did b/rust/candid_parser/tests/assets/not_func.did similarity index 100% rename from rust/candid/tests/assets/not_func.did rename to rust/candid_parser/tests/assets/not_func.did diff --git a/rust/candid/tests/assets/not_serv.did b/rust/candid_parser/tests/assets/not_serv.did similarity index 100% rename from rust/candid/tests/assets/not_serv.did rename to rust/candid_parser/tests/assets/not_serv.did diff --git a/rust/candid/tests/assets/ok/actor.d.ts b/rust/candid_parser/tests/assets/ok/actor.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/actor.d.ts rename to rust/candid_parser/tests/assets/ok/actor.d.ts diff --git a/rust/candid/tests/assets/ok/actor.did b/rust/candid_parser/tests/assets/ok/actor.did similarity index 100% rename from rust/candid/tests/assets/ok/actor.did rename to rust/candid_parser/tests/assets/ok/actor.did diff --git a/rust/candid/tests/assets/ok/actor.js b/rust/candid_parser/tests/assets/ok/actor.js similarity index 100% rename from rust/candid/tests/assets/ok/actor.js rename to rust/candid_parser/tests/assets/ok/actor.js diff --git a/rust/candid/tests/assets/ok/actor.mo b/rust/candid_parser/tests/assets/ok/actor.mo similarity index 100% rename from rust/candid/tests/assets/ok/actor.mo rename to rust/candid_parser/tests/assets/ok/actor.mo diff --git a/rust/candid/tests/assets/ok/actor.rs b/rust/candid_parser/tests/assets/ok/actor.rs similarity index 100% rename from rust/candid/tests/assets/ok/actor.rs rename to rust/candid_parser/tests/assets/ok/actor.rs diff --git a/rust/candid/tests/assets/ok/bad_comment.fail b/rust/candid_parser/tests/assets/ok/bad_comment.fail similarity index 100% rename from rust/candid/tests/assets/ok/bad_comment.fail rename to rust/candid_parser/tests/assets/ok/bad_comment.fail diff --git a/rust/candid/tests/assets/ok/bad_import.fail b/rust/candid_parser/tests/assets/ok/bad_import.fail similarity index 100% rename from rust/candid/tests/assets/ok/bad_import.fail rename to rust/candid_parser/tests/assets/ok/bad_import.fail diff --git a/rust/candid/tests/assets/ok/class.d.ts b/rust/candid_parser/tests/assets/ok/class.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/class.d.ts rename to rust/candid_parser/tests/assets/ok/class.d.ts diff --git a/rust/candid/tests/assets/ok/class.did b/rust/candid_parser/tests/assets/ok/class.did similarity index 100% rename from rust/candid/tests/assets/ok/class.did rename to rust/candid_parser/tests/assets/ok/class.did diff --git a/rust/candid/tests/assets/ok/class.js b/rust/candid_parser/tests/assets/ok/class.js similarity index 100% rename from rust/candid/tests/assets/ok/class.js rename to rust/candid_parser/tests/assets/ok/class.js diff --git a/rust/candid/tests/assets/ok/class.mo b/rust/candid_parser/tests/assets/ok/class.mo similarity index 100% rename from rust/candid/tests/assets/ok/class.mo rename to rust/candid_parser/tests/assets/ok/class.mo diff --git a/rust/candid/tests/assets/ok/class.rs b/rust/candid_parser/tests/assets/ok/class.rs similarity index 100% rename from rust/candid/tests/assets/ok/class.rs rename to rust/candid_parser/tests/assets/ok/class.rs diff --git a/rust/candid/tests/assets/ok/collision_fields.fail b/rust/candid_parser/tests/assets/ok/collision_fields.fail similarity index 100% rename from rust/candid/tests/assets/ok/collision_fields.fail rename to rust/candid_parser/tests/assets/ok/collision_fields.fail diff --git a/rust/candid/tests/assets/ok/collision_fields2.fail b/rust/candid_parser/tests/assets/ok/collision_fields2.fail similarity index 100% rename from rust/candid/tests/assets/ok/collision_fields2.fail rename to rust/candid_parser/tests/assets/ok/collision_fields2.fail diff --git a/rust/candid/tests/assets/ok/comment.d.ts b/rust/candid_parser/tests/assets/ok/comment.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/comment.d.ts rename to rust/candid_parser/tests/assets/ok/comment.d.ts diff --git a/rust/candid/tests/assets/ok/comment.did b/rust/candid_parser/tests/assets/ok/comment.did similarity index 100% rename from rust/candid/tests/assets/ok/comment.did rename to rust/candid_parser/tests/assets/ok/comment.did diff --git a/rust/candid/tests/assets/ok/comment.js b/rust/candid_parser/tests/assets/ok/comment.js similarity index 100% rename from rust/candid/tests/assets/ok/comment.js rename to rust/candid_parser/tests/assets/ok/comment.js diff --git a/rust/candid/tests/assets/ok/comment.mo b/rust/candid_parser/tests/assets/ok/comment.mo similarity index 100% rename from rust/candid/tests/assets/ok/comment.mo rename to rust/candid_parser/tests/assets/ok/comment.mo diff --git a/rust/candid/tests/assets/ok/comment.rs b/rust/candid_parser/tests/assets/ok/comment.rs similarity index 100% rename from rust/candid/tests/assets/ok/comment.rs rename to rust/candid_parser/tests/assets/ok/comment.rs diff --git a/rust/candid/tests/assets/ok/cyclic.d.ts b/rust/candid_parser/tests/assets/ok/cyclic.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/cyclic.d.ts rename to rust/candid_parser/tests/assets/ok/cyclic.d.ts diff --git a/rust/candid/tests/assets/ok/cyclic.did b/rust/candid_parser/tests/assets/ok/cyclic.did similarity index 100% rename from rust/candid/tests/assets/ok/cyclic.did rename to rust/candid_parser/tests/assets/ok/cyclic.did diff --git a/rust/candid/tests/assets/ok/cyclic.js b/rust/candid_parser/tests/assets/ok/cyclic.js similarity index 100% rename from rust/candid/tests/assets/ok/cyclic.js rename to rust/candid_parser/tests/assets/ok/cyclic.js diff --git a/rust/candid/tests/assets/ok/cyclic.mo b/rust/candid_parser/tests/assets/ok/cyclic.mo similarity index 100% rename from rust/candid/tests/assets/ok/cyclic.mo rename to rust/candid_parser/tests/assets/ok/cyclic.mo diff --git a/rust/candid/tests/assets/ok/cyclic.rs b/rust/candid_parser/tests/assets/ok/cyclic.rs similarity index 100% rename from rust/candid/tests/assets/ok/cyclic.rs rename to rust/candid_parser/tests/assets/ok/cyclic.rs diff --git a/rust/candid/tests/assets/ok/escape.d.ts b/rust/candid_parser/tests/assets/ok/escape.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/escape.d.ts rename to rust/candid_parser/tests/assets/ok/escape.d.ts diff --git a/rust/candid/tests/assets/ok/escape.did b/rust/candid_parser/tests/assets/ok/escape.did similarity index 100% rename from rust/candid/tests/assets/ok/escape.did rename to rust/candid_parser/tests/assets/ok/escape.did diff --git a/rust/candid/tests/assets/ok/escape.js b/rust/candid_parser/tests/assets/ok/escape.js similarity index 100% rename from rust/candid/tests/assets/ok/escape.js rename to rust/candid_parser/tests/assets/ok/escape.js diff --git a/rust/candid/tests/assets/ok/escape.rs b/rust/candid_parser/tests/assets/ok/escape.rs similarity index 100% rename from rust/candid/tests/assets/ok/escape.rs rename to rust/candid_parser/tests/assets/ok/escape.rs diff --git a/rust/candid/tests/assets/ok/example.d.ts b/rust/candid_parser/tests/assets/ok/example.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/example.d.ts rename to rust/candid_parser/tests/assets/ok/example.d.ts diff --git a/rust/candid/tests/assets/ok/example.did b/rust/candid_parser/tests/assets/ok/example.did similarity index 100% rename from rust/candid/tests/assets/ok/example.did rename to rust/candid_parser/tests/assets/ok/example.did diff --git a/rust/candid/tests/assets/ok/example.js b/rust/candid_parser/tests/assets/ok/example.js similarity index 100% rename from rust/candid/tests/assets/ok/example.js rename to rust/candid_parser/tests/assets/ok/example.js diff --git a/rust/candid/tests/assets/ok/example.mo b/rust/candid_parser/tests/assets/ok/example.mo similarity index 100% rename from rust/candid/tests/assets/ok/example.mo rename to rust/candid_parser/tests/assets/ok/example.mo diff --git a/rust/candid/tests/assets/ok/example.rs b/rust/candid_parser/tests/assets/ok/example.rs similarity index 100% rename from rust/candid/tests/assets/ok/example.rs rename to rust/candid_parser/tests/assets/ok/example.rs diff --git a/rust/candid/tests/assets/ok/fieldnat.d.ts b/rust/candid_parser/tests/assets/ok/fieldnat.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/fieldnat.d.ts rename to rust/candid_parser/tests/assets/ok/fieldnat.d.ts diff --git a/rust/candid/tests/assets/ok/fieldnat.did b/rust/candid_parser/tests/assets/ok/fieldnat.did similarity index 100% rename from rust/candid/tests/assets/ok/fieldnat.did rename to rust/candid_parser/tests/assets/ok/fieldnat.did diff --git a/rust/candid/tests/assets/ok/fieldnat.js b/rust/candid_parser/tests/assets/ok/fieldnat.js similarity index 100% rename from rust/candid/tests/assets/ok/fieldnat.js rename to rust/candid_parser/tests/assets/ok/fieldnat.js diff --git a/rust/candid/tests/assets/ok/fieldnat.mo b/rust/candid_parser/tests/assets/ok/fieldnat.mo similarity index 100% rename from rust/candid/tests/assets/ok/fieldnat.mo rename to rust/candid_parser/tests/assets/ok/fieldnat.mo diff --git a/rust/candid/tests/assets/ok/fieldnat.rs b/rust/candid_parser/tests/assets/ok/fieldnat.rs similarity index 100% rename from rust/candid/tests/assets/ok/fieldnat.rs rename to rust/candid_parser/tests/assets/ok/fieldnat.rs diff --git a/rust/candid/tests/assets/ok/invalid_cyclic.fail b/rust/candid_parser/tests/assets/ok/invalid_cyclic.fail similarity index 100% rename from rust/candid/tests/assets/ok/invalid_cyclic.fail rename to rust/candid_parser/tests/assets/ok/invalid_cyclic.fail diff --git a/rust/candid/tests/assets/ok/keyword.d.ts b/rust/candid_parser/tests/assets/ok/keyword.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/keyword.d.ts rename to rust/candid_parser/tests/assets/ok/keyword.d.ts diff --git a/rust/candid/tests/assets/ok/keyword.did b/rust/candid_parser/tests/assets/ok/keyword.did similarity index 100% rename from rust/candid/tests/assets/ok/keyword.did rename to rust/candid_parser/tests/assets/ok/keyword.did diff --git a/rust/candid/tests/assets/ok/keyword.js b/rust/candid_parser/tests/assets/ok/keyword.js similarity index 100% rename from rust/candid/tests/assets/ok/keyword.js rename to rust/candid_parser/tests/assets/ok/keyword.js diff --git a/rust/candid/tests/assets/ok/keyword.mo b/rust/candid_parser/tests/assets/ok/keyword.mo similarity index 100% rename from rust/candid/tests/assets/ok/keyword.mo rename to rust/candid_parser/tests/assets/ok/keyword.mo diff --git a/rust/candid/tests/assets/ok/keyword.rs b/rust/candid_parser/tests/assets/ok/keyword.rs similarity index 100% rename from rust/candid/tests/assets/ok/keyword.rs rename to rust/candid_parser/tests/assets/ok/keyword.rs diff --git a/rust/candid/tests/assets/ok/management.d.ts b/rust/candid_parser/tests/assets/ok/management.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/management.d.ts rename to rust/candid_parser/tests/assets/ok/management.d.ts diff --git a/rust/candid/tests/assets/ok/management.did b/rust/candid_parser/tests/assets/ok/management.did similarity index 100% rename from rust/candid/tests/assets/ok/management.did rename to rust/candid_parser/tests/assets/ok/management.did diff --git a/rust/candid/tests/assets/ok/management.js b/rust/candid_parser/tests/assets/ok/management.js similarity index 100% rename from rust/candid/tests/assets/ok/management.js rename to rust/candid_parser/tests/assets/ok/management.js diff --git a/rust/candid/tests/assets/ok/management.mo b/rust/candid_parser/tests/assets/ok/management.mo similarity index 100% rename from rust/candid/tests/assets/ok/management.mo rename to rust/candid_parser/tests/assets/ok/management.mo diff --git a/rust/candid/tests/assets/ok/management.rs b/rust/candid_parser/tests/assets/ok/management.rs similarity index 100% rename from rust/candid/tests/assets/ok/management.rs rename to rust/candid_parser/tests/assets/ok/management.rs diff --git a/rust/candid/tests/assets/ok/not_func.fail b/rust/candid_parser/tests/assets/ok/not_func.fail similarity index 100% rename from rust/candid/tests/assets/ok/not_func.fail rename to rust/candid_parser/tests/assets/ok/not_func.fail diff --git a/rust/candid/tests/assets/ok/not_serv.fail b/rust/candid_parser/tests/assets/ok/not_serv.fail similarity index 100% rename from rust/candid/tests/assets/ok/not_serv.fail rename to rust/candid_parser/tests/assets/ok/not_serv.fail diff --git a/rust/candid/tests/assets/ok/oneway.fail b/rust/candid_parser/tests/assets/ok/oneway.fail similarity index 100% rename from rust/candid/tests/assets/ok/oneway.fail rename to rust/candid_parser/tests/assets/ok/oneway.fail diff --git a/rust/candid/tests/assets/ok/recursion.d.ts b/rust/candid_parser/tests/assets/ok/recursion.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/recursion.d.ts rename to rust/candid_parser/tests/assets/ok/recursion.d.ts diff --git a/rust/candid/tests/assets/ok/recursion.did b/rust/candid_parser/tests/assets/ok/recursion.did similarity index 100% rename from rust/candid/tests/assets/ok/recursion.did rename to rust/candid_parser/tests/assets/ok/recursion.did diff --git a/rust/candid/tests/assets/ok/recursion.js b/rust/candid_parser/tests/assets/ok/recursion.js similarity index 100% rename from rust/candid/tests/assets/ok/recursion.js rename to rust/candid_parser/tests/assets/ok/recursion.js diff --git a/rust/candid/tests/assets/ok/recursion.mo b/rust/candid_parser/tests/assets/ok/recursion.mo similarity index 100% rename from rust/candid/tests/assets/ok/recursion.mo rename to rust/candid_parser/tests/assets/ok/recursion.mo diff --git a/rust/candid/tests/assets/ok/recursion.rs b/rust/candid_parser/tests/assets/ok/recursion.rs similarity index 100% rename from rust/candid/tests/assets/ok/recursion.rs rename to rust/candid_parser/tests/assets/ok/recursion.rs diff --git a/rust/candid/tests/assets/ok/recursive_class.d.ts b/rust/candid_parser/tests/assets/ok/recursive_class.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/recursive_class.d.ts rename to rust/candid_parser/tests/assets/ok/recursive_class.d.ts diff --git a/rust/candid/tests/assets/ok/recursive_class.did b/rust/candid_parser/tests/assets/ok/recursive_class.did similarity index 100% rename from rust/candid/tests/assets/ok/recursive_class.did rename to rust/candid_parser/tests/assets/ok/recursive_class.did diff --git a/rust/candid/tests/assets/ok/recursive_class.js b/rust/candid_parser/tests/assets/ok/recursive_class.js similarity index 100% rename from rust/candid/tests/assets/ok/recursive_class.js rename to rust/candid_parser/tests/assets/ok/recursive_class.js diff --git a/rust/candid/tests/assets/ok/recursive_class.mo b/rust/candid_parser/tests/assets/ok/recursive_class.mo similarity index 100% rename from rust/candid/tests/assets/ok/recursive_class.mo rename to rust/candid_parser/tests/assets/ok/recursive_class.mo diff --git a/rust/candid/tests/assets/ok/recursive_class.rs b/rust/candid_parser/tests/assets/ok/recursive_class.rs similarity index 100% rename from rust/candid/tests/assets/ok/recursive_class.rs rename to rust/candid_parser/tests/assets/ok/recursive_class.rs diff --git a/rust/candid/tests/assets/ok/service.d.ts b/rust/candid_parser/tests/assets/ok/service.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/service.d.ts rename to rust/candid_parser/tests/assets/ok/service.d.ts diff --git a/rust/candid/tests/assets/ok/service.did b/rust/candid_parser/tests/assets/ok/service.did similarity index 100% rename from rust/candid/tests/assets/ok/service.did rename to rust/candid_parser/tests/assets/ok/service.did diff --git a/rust/candid/tests/assets/ok/service.js b/rust/candid_parser/tests/assets/ok/service.js similarity index 100% rename from rust/candid/tests/assets/ok/service.js rename to rust/candid_parser/tests/assets/ok/service.js diff --git a/rust/candid/tests/assets/ok/service.mo b/rust/candid_parser/tests/assets/ok/service.mo similarity index 100% rename from rust/candid/tests/assets/ok/service.mo rename to rust/candid_parser/tests/assets/ok/service.mo diff --git a/rust/candid/tests/assets/ok/service.rs b/rust/candid_parser/tests/assets/ok/service.rs similarity index 100% rename from rust/candid/tests/assets/ok/service.rs rename to rust/candid_parser/tests/assets/ok/service.rs diff --git a/rust/candid/tests/assets/ok/surrogate.fail b/rust/candid_parser/tests/assets/ok/surrogate.fail similarity index 100% rename from rust/candid/tests/assets/ok/surrogate.fail rename to rust/candid_parser/tests/assets/ok/surrogate.fail diff --git a/rust/candid/tests/assets/ok/undefine.fail b/rust/candid_parser/tests/assets/ok/undefine.fail similarity index 100% rename from rust/candid/tests/assets/ok/undefine.fail rename to rust/candid_parser/tests/assets/ok/undefine.fail diff --git a/rust/candid/tests/assets/ok/unicode.d.ts b/rust/candid_parser/tests/assets/ok/unicode.d.ts similarity index 100% rename from rust/candid/tests/assets/ok/unicode.d.ts rename to rust/candid_parser/tests/assets/ok/unicode.d.ts diff --git a/rust/candid/tests/assets/ok/unicode.did b/rust/candid_parser/tests/assets/ok/unicode.did similarity index 100% rename from rust/candid/tests/assets/ok/unicode.did rename to rust/candid_parser/tests/assets/ok/unicode.did diff --git a/rust/candid/tests/assets/ok/unicode.js b/rust/candid_parser/tests/assets/ok/unicode.js similarity index 100% rename from rust/candid/tests/assets/ok/unicode.js rename to rust/candid_parser/tests/assets/ok/unicode.js diff --git a/rust/candid/tests/assets/ok/unicode.rs b/rust/candid_parser/tests/assets/ok/unicode.rs similarity index 100% rename from rust/candid/tests/assets/ok/unicode.rs rename to rust/candid_parser/tests/assets/ok/unicode.rs diff --git a/rust/candid/tests/assets/oneway.did b/rust/candid_parser/tests/assets/oneway.did similarity index 100% rename from rust/candid/tests/assets/oneway.did rename to rust/candid_parser/tests/assets/oneway.did diff --git a/rust/candid/tests/assets/recursion.did b/rust/candid_parser/tests/assets/recursion.did similarity index 100% rename from rust/candid/tests/assets/recursion.did rename to rust/candid_parser/tests/assets/recursion.did diff --git a/rust/candid/tests/assets/recursive_class.did b/rust/candid_parser/tests/assets/recursive_class.did similarity index 100% rename from rust/candid/tests/assets/recursive_class.did rename to rust/candid_parser/tests/assets/recursive_class.did diff --git a/rust/candid/tests/assets/service.did b/rust/candid_parser/tests/assets/service.did similarity index 100% rename from rust/candid/tests/assets/service.did rename to rust/candid_parser/tests/assets/service.did diff --git a/rust/candid/tests/assets/surrogate.did b/rust/candid_parser/tests/assets/surrogate.did similarity index 100% rename from rust/candid/tests/assets/surrogate.did rename to rust/candid_parser/tests/assets/surrogate.did diff --git a/rust/candid/tests/assets/undefine.did b/rust/candid_parser/tests/assets/undefine.did similarity index 100% rename from rust/candid/tests/assets/undefine.did rename to rust/candid_parser/tests/assets/undefine.did diff --git a/rust/candid/tests/assets/unicode.did b/rust/candid_parser/tests/assets/unicode.did similarity index 100% rename from rust/candid/tests/assets/unicode.did rename to rust/candid_parser/tests/assets/unicode.did diff --git a/rust/candid/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs similarity index 91% rename from rust/candid/tests/parse_type.rs rename to rust/candid_parser/tests/parse_type.rs index 85ec1362..8608ced7 100644 --- a/rust/candid/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -1,7 +1,8 @@ -use candid::bindings::{candid as candid_export, javascript, motoko, rust, typescript}; -use candid::parser::types::IDLProg; -use candid::parser::typing::{check_file, check_prog}; +use candid::pretty_printer::compile; use candid::types::TypeEnv; +use candid_parser::bindings::{javascript, motoko, rust, typescript}; +use candid_parser::types::IDLProg; +use candid_parser::typing::{check_file, check_prog}; use goldenfile::Mint; use std::io::Write; use std::path::Path; @@ -29,7 +30,7 @@ service server : { prog.parse::().unwrap(); } -#[test_generator::test_resources("rust/candid/tests/assets/*.did")] +#[test_generator::test_resources("rust/candid_parser/tests/assets/*.did")] fn compiler_test(resource: &str) { let base_path = std::env::current_dir().unwrap().join("tests/assets"); let mut mint = Mint::new(base_path.join("ok")); @@ -41,7 +42,7 @@ fn compiler_test(resource: &str) { Ok((env, actor)) => { { let mut output = mint.new_goldenfile(filename.with_extension("did")).unwrap(); - let content = candid_export::compile(&env, &actor); + let content = compile(&env, &actor); // Type check output let ast = content.parse::().unwrap(); check_prog(&mut TypeEnv::new(), &ast).unwrap(); diff --git a/rust/candid/tests/parse_value.rs b/rust/candid_parser/tests/parse_value.rs similarity index 97% rename from rust/candid/tests/parse_value.rs rename to rust/candid_parser/tests/parse_value.rs index 027401db..fce5297d 100644 --- a/rust/candid/tests/parse_value.rs +++ b/rust/candid_parser/tests/parse_value.rs @@ -1,20 +1,21 @@ use candid::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; use candid::types::{Label, Type, TypeEnv, TypeInner}; use candid::{record, variant, CandidType, Nat}; +use candid_parser::parse_idl_args; fn parse_args(input: &str) -> IDLArgs { - input.parse().unwrap() + parse_idl_args(input).unwrap() } -fn parse_args_err(input: &str) -> candid::Result { - input.parse() +fn parse_args_err(input: &str) -> candid_parser::Result { + parse_idl_args(input) } fn parse_type(input: &str) -> Type { - use candid::parser::types::IDLType; + use candid_parser::types::IDLType; let env = TypeEnv::new(); let ast = input.parse::().unwrap(); - env.ast_to_type(&ast).unwrap() + candid_parser::typing::ast_to_type(&env, &ast).unwrap() } #[test] diff --git a/rust/candid/tests/test_suite.rs b/rust/candid_parser/tests/test_suite.rs similarity index 85% rename from rust/candid/tests/test_suite.rs rename to rust/candid_parser/tests/test_suite.rs index e3a68fa1..83a07133 100644 --- a/rust/candid/tests/test_suite.rs +++ b/rust/candid_parser/tests/test_suite.rs @@ -1,4 +1,4 @@ -use candid::parser::test::{check, Test}; +use candid_parser::test::{check, Test}; #[test_generator::test_resources("test/*.test.did")] fn decode_test(resource: &str) { @@ -13,7 +13,7 @@ fn decode_test(resource: &str) { #[test_generator::test_resources("test/*.test.did")] fn js_test(resource: &str) { - use candid::bindings::javascript::test::test_generate; + use candid_parser::bindings::javascript::test::test_generate; let path = std::env::current_dir() .unwrap() .join("../../") diff --git a/rust/candid/tests/value.rs b/rust/candid_parser/tests/value.rs similarity index 95% rename from rust/candid/tests/value.rs rename to rust/candid_parser/tests/value.rs index fd3f4f61..0e0f4748 100644 --- a/rust/candid/tests/value.rs +++ b/rust/candid_parser/tests/value.rs @@ -1,7 +1,7 @@ -use candid::parser::{types::IDLProg, typing::check_prog}; use candid::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; use candid::types::{Label, TypeEnv}; use candid::{decode_args, decode_one, Decode}; +use candid_parser::{parse_idl_args, types::IDLProg, typing::check_prog}; #[test] fn test_parser() { @@ -36,7 +36,7 @@ service : { let actor = check_prog(&mut env, &ast).unwrap().unwrap(); let method = env.get_method(&actor, "f").unwrap(); { - let args = "(42,42,42,42)".parse::().unwrap(); + let args = parse_idl_args("(42,42,42,42)").unwrap(); let encoded = args.to_bytes_with_types(&env, &method.args).unwrap(); let decoded = IDLArgs::from_bytes(&encoded).unwrap(); assert_eq!( @@ -51,7 +51,7 @@ service : { } { let str = "(opt record { head = 1000; tail = opt record {head = -2000; tail = null}}, variant {a = 42})"; - let args = str.parse::().unwrap(); + let args = parse_idl_args(str).unwrap(); let encoded = args.to_bytes_with_types(&env, &method.rets).unwrap(); let decoded = IDLArgs::from_bytes(&encoded).unwrap(); assert_eq!(decoded.to_string(), "(\n opt record {\n 1_158_359_328 = 1_000 : int16;\n 1_291_237_008 = opt record {\n 1_158_359_328 = -2_000 : int16;\n 1_291_237_008 = null;\n };\n },\n variant { 97 = 42 : nat },\n)"); @@ -120,11 +120,11 @@ fn test_variant() { } fn parse_check(str: &str) { - let args = str.parse::().unwrap(); + let args = parse_idl_args(str).unwrap(); let encoded = args.to_bytes().unwrap(); let decoded = IDLArgs::from_bytes(&encoded).unwrap(); let output = decoded.to_string(); - let back_args = output.parse::().unwrap(); + let back_args = parse_idl_args(&output).unwrap(); let annotated_args = args .annotate_types(true, &TypeEnv::new(), &back_args.get_types()) .unwrap(); diff --git a/rust/ic_principal/Cargo.toml b/rust/ic_principal/Cargo.toml new file mode 100644 index 00000000..8164b80d --- /dev/null +++ b/rust/ic_principal/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "ic_principal" +version = "0.1.0" +authors = ["DFINITY Stiftung "] +edition = "2021" +homepage = "https://internetcomputer.org/docs/current/references/id-encoding-spec" +documentation = "https://docs.rs/ic_principal" +repository="https://github.com/dfinity/candid" +license = "Apache-2.0" +readme = "README.md" +categories = ["data-structures", "no-std"] +keywords = ["internet-computer", "types", "dfinity"] +include = ["src", "Cargo.toml", "LICENSE", "README.md"] + +[dependencies] +crc32fast = "1.3.0" +data-encoding = "2.3.2" +hex = "0.4.3" +sha2 = "0.10.1" +thiserror = "1.0.30" + +[dev-dependencies] +serde_cbor = "0.11.2" +serde_json = "1.0.74" +serde_test = "1.0.137" +impls = "1" + +[dependencies.serde] +version = "1.0.115" +features = ["derive"] +optional = true + +[dependencies.serde_bytes] +version = "0.11.5" +optional = true + +[features] +# Default features include serde support. +default = ['serde', 'serde_bytes'] diff --git a/rust/ic_principal/LICENSE b/rust/ic_principal/LICENSE new file mode 120000 index 00000000..30cff740 --- /dev/null +++ b/rust/ic_principal/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/rust/ic_principal/README.md b/rust/ic_principal/README.md new file mode 100644 index 00000000..b32805eb --- /dev/null +++ b/rust/ic_principal/README.md @@ -0,0 +1,5 @@ +# Internet Computer Principal + +`Principal` is the data structure for generic identifiers on the Internet Computer. + +Check the [ID encoding specification](https://internetcomputer.org/docs/current/references/id-encoding-spec) for more information. diff --git a/rust/ic_principal/src/lib.rs b/rust/ic_principal/src/lib.rs new file mode 100644 index 00000000..b128ccce --- /dev/null +++ b/rust/ic_principal/src/lib.rs @@ -0,0 +1,360 @@ +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha224}; +use std::convert::TryFrom; +use std::fmt::Write; +use thiserror::Error; + +/// An error happened while encoding, decoding or serializing a [`Principal`]. +#[derive(Error, Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PrincipalError { + #[error("Bytes is longer than 29 bytes.")] + BytesTooLong(), + + #[error("Text must be in valid Base32 encoding.")] + InvalidBase32(), + + #[error("Text is too short.")] + TextTooShort(), + + #[error("Text is too long.")] + TextTooLong(), + + #[error("CRC32 check sequence doesn't match with calculated from Principal bytes.")] + CheckSequenceNotMatch(), + + #[error(r#"Text should be separated by - (dash) every 5 characters: expected "{0}""#)] + AbnormalGrouped(Principal), +} + +/// Generic ID on Internet Computer. +/// +/// Principals are generic identifiers for canisters, users +/// and possibly other concepts in the future. +/// As far as most uses of the IC are concerned they are +/// opaque binary blobs with a length between 0 and 29 bytes, +/// and there is intentionally no mechanism to tell canister ids and user ids apart. +/// +/// Note a principal is not necessarily tied with a public key-pair, +/// yet we need at least a key-pair of a related principal to sign +/// requests. +/// +/// A Principal can be serialized to a byte array ([`Vec`]) or a text +/// representation, but the inner structure of the byte representation +/// is kept private. +/// +/// Example of using a Principal object: +/// ``` +/// use ic_principal::Principal; +/// +/// let text = "aaaaa-aa"; // The management canister ID. +/// let principal = Principal::from_text(text).expect("Could not decode the principal."); +/// assert_eq!(principal.as_slice(), &[]); +/// assert_eq!(principal.to_text(), text); +/// ``` +/// +/// Serialization is enabled with the "serde" feature. It supports serializing +/// to a byte bufer for non-human readable serializer, and a string version for human +/// readable serializers. +/// +/// ``` +/// use ic_principal::Principal; +/// use serde::{Deserialize, Serialize}; +/// use std::str::FromStr; +/// +/// #[derive(Serialize)] +/// struct Data { +/// id: Principal, +/// } +/// +/// let id = Principal::from_str("2chl6-4hpzw-vqaaa-aaaaa-c").unwrap(); +/// +/// // JSON is human readable, so this will serialize to a textual +/// // representation of the Principal. +/// assert_eq!( +/// serde_json::to_string(&Data { id: id.clone() }).unwrap(), +/// r#"{"id":"2chl6-4hpzw-vqaaa-aaaaa-c"}"# +/// ); +/// +/// // CBOR is not human readable, so will serialize to bytes. +/// assert_eq!( +/// serde_cbor::to_vec(&Data { id: id.clone() }).unwrap(), +/// &[161, 98, 105, 100, 73, 239, 205, 171, 0, 0, 0, 0, 0, 1], +/// ); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Principal { + /// Length. + len: u8, + + /// The content buffer. When returning slices this should always be sized according to + /// `len`. + bytes: [u8; Self::MAX_LENGTH_IN_BYTES], +} + +impl Principal { + const MAX_LENGTH_IN_BYTES: usize = 29; + const CRC_LENGTH_IN_BYTES: usize = 4; + + const SELF_AUTHENTICATING_TAG: u8 = 2; + const ANONYMOUS_TAG: u8 = 4; + + /// Construct a [`Principal`] of the IC management canister + pub const fn management_canister() -> Self { + Self { + len: 0, + bytes: [0; Self::MAX_LENGTH_IN_BYTES], + } + } + + /// Construct a self-authenticating ID from public key + pub fn self_authenticating>(public_key: P) -> Self { + let public_key = public_key.as_ref(); + let hash = Sha224::digest(public_key); + let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; + bytes[..Self::MAX_LENGTH_IN_BYTES - 1].copy_from_slice(hash.as_slice()); + bytes[Self::MAX_LENGTH_IN_BYTES - 1] = Self::SELF_AUTHENTICATING_TAG; + + Self { + len: Self::MAX_LENGTH_IN_BYTES as u8, + bytes, + } + } + + /// Construct an anonymous ID. + pub const fn anonymous() -> Self { + let mut bytes = [0; Self::MAX_LENGTH_IN_BYTES]; + bytes[0] = Self::ANONYMOUS_TAG; + Self { len: 1, bytes } + } + + /// Construct a [`Principal`] from a slice of bytes. + /// + /// # Panics + /// + /// Panics if the slice is longer than 29 bytes. + pub const fn from_slice(slice: &[u8]) -> Self { + match Self::try_from_slice(slice) { + Ok(v) => v, + _ => panic!("slice length exceeds capacity"), + } + } + + /// Construct a [`Principal`] from a slice of bytes. + pub const fn try_from_slice(slice: &[u8]) -> Result { + const MAX_LENGTH_IN_BYTES: usize = Principal::MAX_LENGTH_IN_BYTES; + match slice.len() { + len @ 0..=MAX_LENGTH_IN_BYTES => { + let mut bytes = [0; MAX_LENGTH_IN_BYTES]; + let mut i = 0; + while i < len { + bytes[i] = slice[i]; + i += 1; + } + Ok(Self { + len: len as u8, + bytes, + }) + } + _ => Err(PrincipalError::BytesTooLong()), + } + } + + /// Parse a [`Principal`] from text representation. + pub fn from_text>(text: S) -> Result { + // Strategy: Parse very liberally, then pretty-print and compare output + // This is both simpler and yields better error messages + + let mut s = text.as_ref().to_string(); + s.make_ascii_uppercase(); + s.retain(|c| c != '-'); + match data_encoding::BASE32_NOPAD.decode(s.as_bytes()) { + Ok(bytes) => { + if bytes.len() < Self::CRC_LENGTH_IN_BYTES { + return Err(PrincipalError::TextTooShort()); + } + + let crc_bytes = &bytes[..Self::CRC_LENGTH_IN_BYTES]; + let data_bytes = &bytes[Self::CRC_LENGTH_IN_BYTES..]; + if data_bytes.len() > Self::MAX_LENGTH_IN_BYTES { + return Err(PrincipalError::TextTooLong()); + } + + if crc32fast::hash(data_bytes).to_be_bytes() != crc_bytes { + return Err(PrincipalError::CheckSequenceNotMatch()); + } + + // Already checked data_bytes.len() <= MAX_LENGTH_IN_BYTES + // safe to unwrap here + let result = Self::try_from_slice(data_bytes).unwrap(); + let expected = format!("{}", result); + + // In the Spec: + // The textual representation is conventionally printed with lower case letters, + // but parsed case-insensitively. + if text.as_ref().to_ascii_lowercase() != expected { + return Err(PrincipalError::AbnormalGrouped(result)); + } + Ok(result) + } + _ => Err(PrincipalError::InvalidBase32()), + } + } + + /// Convert [`Principal`] to text representation. + pub fn to_text(&self) -> String { + format!("{}", self) + } + + /// Return the [`Principal`]'s underlying slice of bytes. + #[inline] + pub fn as_slice(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } +} + +impl std::fmt::Display for Principal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let blob: &[u8] = self.as_slice(); + + // calc checksum + let checksum = crc32fast::hash(blob); + + // combine blobs + let mut bytes = vec![]; + bytes.extend_from_slice(&checksum.to_be_bytes()); + bytes.extend_from_slice(blob); + + // base32 + let mut s = data_encoding::BASE32_NOPAD.encode(&bytes); + s.make_ascii_lowercase(); + + // write out string with dashes + let mut s = s.as_str(); + while s.len() > 5 { + f.write_str(&s[..5])?; + f.write_char('-')?; + s = &s[5..]; + } + f.write_str(s) + } +} + +impl std::str::FromStr for Principal { + type Err = PrincipalError; + + fn from_str(s: &str) -> Result { + Principal::from_text(s) + } +} + +impl TryFrom<&str> for Principal { + type Error = PrincipalError; + + fn try_from(s: &str) -> Result { + Principal::from_text(s) + } +} + +impl TryFrom> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: Vec) -> Result { + Self::try_from(bytes.as_slice()) + } +} + +impl TryFrom<&Vec> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: &Vec) -> Result { + Self::try_from(bytes.as_slice()) + } +} + +impl TryFrom<&[u8]> for Principal { + type Error = PrincipalError; + + fn try_from(bytes: &[u8]) -> Result { + Self::try_from_slice(bytes) + } +} + +impl AsRef<[u8]> for Principal { + fn as_ref(&self) -> &[u8] { + self.as_slice() + } +} + +// Serialization +#[cfg(feature = "serde")] +impl serde::Serialize for Principal { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + self.to_text().serialize(serializer) + } else { + serializer.serialize_bytes(self.as_slice()) + } + } +} + +// Deserialization +#[cfg(feature = "serde")] +mod deserialize { + use super::Principal; + use std::convert::TryFrom; + + // Simple visitor for deserialization from bytes. We don't support other number types + // as there's no need for it. + pub(super) struct PrincipalVisitor; + + impl<'de> serde::de::Visitor<'de> for PrincipalVisitor { + type Value = super::Principal; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("bytes or string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Principal::from_text(v).map_err(E::custom) + } + + fn visit_bytes(self, value: &[u8]) -> Result + where + E: serde::de::Error, + { + Principal::try_from(value).map_err(E::custom) + } + /// This visitor should only be used by the Candid crate. + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + if v.is_empty() || v[0] != 2u8 { + Err(E::custom("Not called by Candid")) + } else { + Principal::try_from(&v[1..]).map_err(E::custom) + } + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Principal { + fn deserialize>(deserializer: D) -> Result { + use serde::de::Error; + if deserializer.is_human_readable() { + deserializer + .deserialize_str(deserialize::PrincipalVisitor) + .map_err(D::Error::custom) + } else { + deserializer + .deserialize_bytes(deserialize::PrincipalVisitor) + .map_err(D::Error::custom) + } + } +} diff --git a/rust/candid/tests/principal.rs b/rust/ic_principal/tests/principal.rs similarity index 98% rename from rust/candid/tests/principal.rs rename to rust/ic_principal/tests/principal.rs index ec402be6..25393889 100644 --- a/rust/candid/tests/principal.rs +++ b/rust/ic_principal/tests/principal.rs @@ -1,6 +1,7 @@ -#![allow(deprecated)] +// #![allow(deprecated)] -use candid::types::principal::{Principal, PrincipalError}; +use ic_principal::Principal; +use ic_principal::PrincipalError; const MANAGEMENT_CANISTER_BYTES: [u8; 0] = []; const MANAGEMENT_CANISTER_TEXT: &str = "aaaaa-aa"; diff --git a/tools/didc/Cargo.toml b/tools/didc/Cargo.toml index f1cf5e91..ff4399fd 100644 --- a/tools/didc/Cargo.toml +++ b/tools/didc/Cargo.toml @@ -5,7 +5,8 @@ authors = ["DFINITY Team"] edition = "2021" [dependencies] -candid = { path = "../../rust/candid", features = ["all"] } +candid = { path = "../../rust/candid" } +candid_parser = { path = "../../rust/candid_parser", features = ["all"] } clap = { version = "4.3", features = ["derive"] } pretty-hex = "0.2.1" hex = "0.4.2" diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index 9a349fab..a13d341d 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -1,9 +1,11 @@ use anyhow::{bail, Result}; -use candid::{ - parser::types::{IDLType, IDLTypes}, - pretty_check_file, pretty_parse, - types::Type, - Error, IDLArgs, TypeEnv, +use candid::{types::Type, IDLArgs, TypeEnv}; +use candid_parser::{ + error::pretty_diagnose, + parse_idl_args, parse_idl_value, pretty_check_file, pretty_parse, + types::{IDLType, IDLTypes}, + typing::ast_to_type, + Error, }; use clap::Parser; use std::collections::HashSet; @@ -78,6 +80,7 @@ enum Command { /// Specifies target language lang: String, #[clap(short, long, requires("method"))] + #[clap(value_parser = parse_args)] /// Specifies input arguments for a method call, mocking the return result args: Option, }, @@ -113,7 +116,7 @@ impl TypeAnnotation { fn is_empty(&self) -> bool { self.tys.is_none() && self.method.is_none() } - fn get_types(&self, mode: Mode) -> candid::Result<(TypeEnv, Vec)> { + fn get_types(&self, mode: Mode) -> candid_parser::Result<(TypeEnv, Vec)> { let (env, actor) = if let Some(ref file) = self.defs { pretty_check_file(file)? } else { @@ -124,7 +127,7 @@ impl TypeAnnotation { (Some(tys), None) => { let mut types = Vec::new(); for ty in tys.args.iter() { - types.push(env.ast_to_type(ty)?); + types.push(ast_to_type(&env, ty)?); } Ok((env, types)) } @@ -145,7 +148,10 @@ impl TypeAnnotation { } fn parse_args(str: &str) -> Result { - pretty_parse("candid arguments", str) + parse_idl_args(str).map_err(|e| { + let _ = pretty_diagnose("candid arguments", str, &e); + e + }) } fn parse_types(str: &str) -> Result { pretty_parse("type annotations", str) @@ -183,25 +189,25 @@ fn main() -> Result<()> { } else { (TypeEnv::new(), None) }; - let ty1 = env.ast_to_type(&ty1)?; - let ty2 = env.ast_to_type(&ty2)?; + let ty1 = ast_to_type(&env, &ty1)?; + let ty2 = ast_to_type(&env, &ty2)?; candid::types::subtype::subtype(&mut HashSet::new(), &env, &ty1, &ty2)?; } Command::Bind { input, target } => { let (env, actor) = pretty_check_file(&input)?; let content = match target.as_str() { - "js" => candid::bindings::javascript::compile(&env, &actor), - "ts" => candid::bindings::typescript::compile(&env, &actor), - "did" => candid::bindings::candid::compile(&env, &actor), - "mo" => candid::bindings::motoko::compile(&env, &actor), + "js" => candid_parser::bindings::javascript::compile(&env, &actor), + "ts" => candid_parser::bindings::typescript::compile(&env, &actor), + "did" => candid::pretty_printer::compile(&env, &actor), + "mo" => candid_parser::bindings::motoko::compile(&env, &actor), "rs" => { - let config = candid::bindings::rust::Config::new(); - candid::bindings::rust::compile(&config, &env, &actor) + let config = candid_parser::bindings::rust::Config::new(); + candid_parser::bindings::rust::compile(&config, &env, &actor) } "rs-agent" => { - let mut config = candid::bindings::rust::Config::new(); - config.target = candid::bindings::rust::Target::Agent; - candid::bindings::rust::compile(&config, &env, &actor) + let mut config = candid_parser::bindings::rust::Config::new(); + config.target = candid_parser::bindings::rust::Target::Agent; + candid_parser::bindings::rust::compile(&config, &env, &actor) } _ => unreachable!(), }; @@ -210,11 +216,11 @@ fn main() -> Result<()> { Command::Test { input, target } => { let test = std::fs::read_to_string(&input) .map_err(|_| Error::msg(format!("could not read file {}", input.display())))?; - let ast = pretty_parse::(input.to_str().unwrap(), &test)?; + let ast = pretty_parse::(input.to_str().unwrap(), &test)?; let content = match target.as_str() { - "js" => candid::bindings::javascript::test::test_generate(ast), + "js" => candid_parser::bindings::javascript::test::test_generate(ast), "did" => { - candid::parser::test::check(ast)?; + candid_parser::test::check(ast)?; "".to_string() } _ => unreachable!(), @@ -248,7 +254,7 @@ fn main() -> Result<()> { "blob" => { let mut res = String::new(); for ch in bytes.iter() { - res.push_str(&candid::bindings::candid::value::pp_char(*ch)); + res.push_str(&candid::pretty_printer::value::pp_char(*ch)); } format!("blob \"{res}\"") } @@ -273,7 +279,10 @@ fn main() -> Result<()> { )?, "blob" => { use candid::types::value::IDLValue; - match pretty_parse::("blob", &blob)? { + match parse_idl_value(&blob).map_err(|e| { + let _ = pretty_diagnose("blob", &blob, &e); + e + })? { IDLValue::Vec(vec) => vec .iter() .map(|v| { @@ -304,7 +313,7 @@ fn main() -> Result<()> { file, args, } => { - use candid::parser::configs::Configs; + use candid_parser::configs::Configs; use rand::Rng; let (env, types) = if args.is_some() { annotate.get_types(Mode::Decode)? @@ -335,12 +344,12 @@ fn main() -> Result<()> { let mut rng = rand::thread_rng(); (0..2048).map(|_| rng.gen::()).collect() }; - let args = IDLArgs::any(&seed, &config, &env, &types)?; + let args = candid_parser::random::any(&seed, &config, &env, &types)?; match lang.as_str() { "did" => println!("{args}"), "js" => println!( "{}", - candid::bindings::javascript::value::pp_args(&args).pretty(80) + candid_parser::bindings::javascript::value::pp_args(&args).pretty(80) ), _ => unreachable!(), }