diff --git a/Cargo.toml b/Cargo.toml index 01d484e5a..dd1cbc13c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ async-trait = "0.1.67" btdht = { git = "https://github.com/equalitie/btdht.git", rev = "4b8dc478e3e5f011a45b8f50c424519019f7b70d" } bytes = "1.4.0" camino = "1.0.9" +clap = { version = "4.1.8", features = ["derive"] } futures-util = { version = "0.3.27", default-features = false } num_enum = { version = "0.5.11", default-features = false } once_cell = "1.12.0" diff --git a/bindgen/Cargo.toml b/bindgen/Cargo.toml index 68292ef3a..ff90c3aa8 100644 --- a/bindgen/Cargo.toml +++ b/bindgen/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "ouisync-bindgen" +description = "Bindings generator for the ouisync library" publish = false authors.workspace = true edition.workspace = true @@ -13,5 +14,7 @@ name = "bindgen" path = "src/main.rs" [dependencies] -cbindgen = "0.20.0" -env_logger = { version = "0.10.0", optional = true } +clap = { workspace = true } +heck = "0.4.1" +syn = { version = "2.0.33", default-features = false, features = ["parsing", "full", "extra-traits"] } +thiserror = { workspace = true } diff --git a/bindgen/src/dart.rs b/bindgen/src/dart.rs new file mode 100644 index 000000000..9966872a7 --- /dev/null +++ b/bindgen/src/dart.rs @@ -0,0 +1,67 @@ +use crate::parse::{Enum, Source}; +use heck::AsLowerCamelCase; +use std::io::{self, Write}; + +pub(crate) fn generate(source: &Source, out: &mut dyn Write) -> io::Result<()> { + for (name, value) in &source.enums { + generate_enum(name, value, out)?; + } + + Ok(()) +} + +fn generate_enum(name: &str, value: &Enum, out: &mut dyn Write) -> io::Result<()> { + writeln!(out, "enum {name} {{")?; + + for variant in &value.variants { + writeln!(out, " {},", AsLowerCamelCase(&variant.name))?; + } + + writeln!(out, " ;")?; + writeln!(out)?; + + // decode + writeln!(out, " static {} decode(int n) {{", name)?; + writeln!(out, " switch (n) {{")?; + + for variant in &value.variants { + writeln!( + out, + " case {}: return {}.{};", + variant.value, + name, + AsLowerCamelCase(&variant.name) + )?; + } + + writeln!( + out, + " default: throw ArgumentError('invalid value: $n');" + )?; + writeln!(out, " }}")?; + writeln!(out, " }}")?; + writeln!(out)?; + + // encode + writeln!(out, " int encode() {{")?; + writeln!(out, " switch (this) {{")?; + + for variant in &value.variants { + writeln!( + out, + " case {}.{}: return {};", + name, + AsLowerCamelCase(&variant.name), + variant.value + )?; + } + + writeln!(out, " }}")?; + writeln!(out, " }}")?; + writeln!(out)?; + + writeln!(out, "}}")?; + writeln!(out)?; + + Ok(()) +} diff --git a/bindgen/src/main.rs b/bindgen/src/main.rs index baf97503e..85178f2eb 100644 --- a/bindgen/src/main.rs +++ b/bindgen/src/main.rs @@ -1,27 +1,43 @@ -use cbindgen::{Builder, EnumConfig, Language, RenameRule}; -use std::path::Path; +mod dart; +mod parse; + +use clap::{Parser, ValueEnum}; +use parse::{parse_file, Source}; +use std::io; fn main() -> Result<(), Box> { - // Generate the C bindings header - - #[cfg(feature = "env_logger")] - env_logger::init(); - - let output_path = Path::new("target").join("bindings.h"); - - Builder::new() - .with_config(cbindgen::Config { - language: Language::C, - enumeration: EnumConfig { - rename_variants: RenameRule::CamelCase, - ..Default::default() - }, - ..Default::default() - }) - .with_src(Path::new("ffi").join("src").join("lib.rs")) - .with_src(Path::new("bridge").join("src").join("constants.rs")) - .generate()? - .write_to_file(output_path); + let options = Options::parse(); + + let source_files = [ + "bridge/src/protocol/mod.rs", + "ffi/src/lib.rs", + "lib/src/access_control/access_mode.rs", + "lib/src/directory/entry_type.rs", + ]; + let mut source = Source::new(); + + for source_file in source_files { + parse_file(source_file, &mut source)?; + } + + match options.language { + Language::Dart => dart::generate(&source, &mut io::stdout())?, + Language::Kotlin => todo!(), + } Ok(()) } + +#[derive(Parser, Debug)] +#[command(about)] +struct Options { + /// Language to generate the bindings for + #[arg(short, long)] + language: Language, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum Language { + Dart, + Kotlin, +} diff --git a/bindgen/src/parse.rs b/bindgen/src/parse.rs new file mode 100644 index 000000000..d784692f5 --- /dev/null +++ b/bindgen/src/parse.rs @@ -0,0 +1,247 @@ +use std::{ + collections::BTreeMap, + fs, io, + path::{Path, PathBuf}, + str::FromStr, +}; +use syn::{Attribute, BinOp, Expr, ExprBinary, Fields, Item, ItemEnum, Lit, Meta, Visibility}; +use thiserror::Error; + +#[derive(Default, Debug)] +pub(crate) struct Source { + pub enums: BTreeMap, +} + +impl Source { + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Debug)] +pub(crate) struct Enum { + pub repr: EnumRepr, + pub variants: Vec, +} + +#[derive(Debug)] +pub(crate) struct EnumVariant { + pub name: String, + pub value: u64, +} + +#[derive(Debug)] +pub(crate) enum EnumRepr { + U8, + U16, + U32, + U64, +} + +impl FromStr for EnumRepr { + type Err = UnsupportedEnumRepr; + + fn from_str(input: &str) -> Result { + let input = input.trim(); + + match input { + "u8" => Ok(Self::U8), + "u16" => Ok(Self::U16), + "u32" => Ok(Self::U32), + "u64" => Ok(Self::U64), + _ => Err(UnsupportedEnumRepr), + } + } +} + +#[derive(Debug)] +pub(crate) struct UnsupportedEnumRepr; + +#[derive(Error, Debug)] +pub(crate) enum ParseError { + #[error("mod '{name}' not found in '{file}'")] + ModNotFound { file: PathBuf, name: String }, + #[error("syn error")] + Syn(#[from] syn::Error), + #[error("io error")] + Io(#[from] io::Error), +} + +pub(crate) fn parse_file(path: impl AsRef, source: &mut Source) -> Result<(), ParseError> { + let path = path.as_ref(); + let content = fs::read_to_string(path)?; + let file = syn::parse_file(&content)?; + + parse_mod(path, file.items, source) +} + +fn parse_file_if_exists(path: &Path, source: &mut Source) -> Result { + match parse_file(path, source) { + Ok(()) => Ok(true), + Err(ParseError::Io(error)) if error.kind() == io::ErrorKind::NotFound => Ok(false), + Err(error) => Err(error), + } +} + +fn parse_mod(path: &Path, items: Vec, source: &mut Source) -> Result<(), ParseError> { + for item in items { + match item { + Item::Enum(item) => { + let name = item.ident.to_string(); + + if let Some(value) = parse_enum(item) { + source.enums.insert(name, value); + } + } + Item::Mod(item) => match item.content { + Some((_, items)) => parse_mod(path, items, source)?, + None => parse_mod_in_file(path, &item.ident.to_string(), source)?, + }, + _ => (), + } + } + + Ok(()) +} + +fn parse_mod_in_file( + parent_path: &Path, + name: &str, + source: &mut Source, +) -> Result<(), ParseError> { + // Try ident.rs + let path = parent_path + .parent() + .unwrap() + .join(name) + .with_extension("rs"); + + if parse_file_if_exists(&path, source)? { + return Ok(()); + } + + // Try ident/mod.rs + let path = parent_path.parent().unwrap().join(name).join("mod.rs"); + + if parse_file_if_exists(&path, source)? { + return Ok(()); + } + + // Try self/ident.rs + let path = parent_path + .with_extension("") + .join(name) + .with_extension("rs"); + + if parse_file_if_exists(&path, source)? { + return Ok(()); + } + + Err(ParseError::ModNotFound { + file: path.to_owned(), + name: name.to_owned(), + }) +} + +fn parse_enum(item: ItemEnum) -> Option { + if !matches!(item.vis, Visibility::Public(_)) { + return None; + } + + let Some(repr) = extract_repr(&item.attrs) else { + return None; + }; + + let Ok(repr) = repr.parse::() else { + return None; + }; + + let mut next_value = 0; + let mut variants = Vec::new(); + + for variant in item.variants { + if !matches!(variant.fields, Fields::Unit) { + println!( + "enum variant with fields not supported: {}::{}", + item.ident, variant.ident + ); + return None; + } + + let value = if let Some((_, expr)) = variant.discriminant { + parse_const_int_expr(expr)? + } else { + next_value + }; + + next_value = value + 1; + + variants.push(EnumVariant { + name: variant.ident.to_string(), + value, + }); + } + + Some(Enum { repr, variants }) +} + +fn parse_const_int_expr(expr: Expr) -> Option { + match expr { + Expr::Lit(expr) => parse_int_lit(expr.lit), + Expr::Binary(expr) => parse_const_int_binary_expr(expr), + _ => None, + } +} + +fn parse_int_lit(lit: Lit) -> Option { + match lit { + Lit::Int(lit) => { + if let Ok(value) = lit.base10_parse() { + Some(value) + } else { + println!("int literal overflow: {}", lit.base10_digits()); + None + } + } + Lit::Byte(lit) => Some(lit.value() as _), + _ => { + println!("not an int or byte literal"); + None + } + } +} + +fn parse_const_int_binary_expr(expr: ExprBinary) -> Option { + let lhs = parse_const_int_expr(*expr.left)?; + let rhs = parse_const_int_expr(*expr.right)?; + + match expr.op { + BinOp::Add(_) => Some(lhs + rhs), + BinOp::Sub(_) => Some(lhs - rhs), + BinOp::Mul(_) => Some(lhs * rhs), + BinOp::Div(_) => Some(lhs / rhs), + BinOp::Rem(_) => Some(lhs % rhs), + BinOp::BitXor(_) => Some(lhs ^ rhs), + BinOp::BitAnd(_) => Some(lhs & rhs), + BinOp::BitOr(_) => Some(lhs & rhs), + BinOp::Shl(_) => Some(lhs << rhs), + BinOp::Shr(_) => Some(lhs >> rhs), + _ => None, + } +} + +fn extract_repr(attrs: &[Attribute]) -> Option { + for attr in attrs { + let Meta::List(meta) = &attr.meta else { + continue; + }; + + if !meta.path.is_ident("repr") { + continue; + } + + return Some(meta.tokens.to_string()); + } + + None +} diff --git a/bridge/src/constants.rs b/bridge/src/constants.rs deleted file mode 100644 index e48e27319..000000000 --- a/bridge/src/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const NETWORK_EVENT_PROTOCOL_VERSION_MISMATCH: u8 = 0; -pub const NETWORK_EVENT_PEER_SET_CHANGE: u8 = 1; diff --git a/bridge/src/lib.rs b/bridge/src/lib.rs index 4304ab659..81bdd6aa5 100644 --- a/bridge/src/lib.rs +++ b/bridge/src/lib.rs @@ -1,5 +1,4 @@ pub mod config; -pub mod constants; pub mod device_id; pub mod dht_contacts; pub mod logger; diff --git a/bridge/src/protocol/mod.rs b/bridge/src/protocol/mod.rs index 6943a2c85..955ddb7df 100644 --- a/bridge/src/protocol/mod.rs +++ b/bridge/src/protocol/mod.rs @@ -1,6 +1,5 @@ pub mod remote; -use crate::constants::{NETWORK_EVENT_PEER_SET_CHANGE, NETWORK_EVENT_PROTOCOL_VERSION_MISMATCH}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde::{Deserialize, Serialize}; @@ -39,8 +38,8 @@ pub enum Notification { #[repr(u8)] #[serde(into = "u8", try_from = "u8")] pub enum NetworkEvent { - ProtocolVersionMismatch = NETWORK_EVENT_PROTOCOL_VERSION_MISMATCH, - PeerSetChange = NETWORK_EVENT_PEER_SET_CHANGE, + ProtocolVersionMismatch = 0, + PeerSetChange = 1, } #[cfg(test)] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 19a5acbd2..ed2cb2bb9 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,7 +19,7 @@ anyhow = "1.0.57" async-trait = { workspace = true } bytes = "1.4.0" camino = { workspace = true } -clap = { version = "4.1.8", features = ["derive"] } +clap = { workspace = true } dirs = "4.0.0" futures-util = { workspace = true } hyper = { version = "0.14.27", features = ["server", "http1", "http2"] } diff --git a/ffi/src/constants.rs b/ffi/src/constants.rs deleted file mode 100644 index b57e408b5..000000000 --- a/ffi/src/constants.rs +++ /dev/null @@ -1,33 +0,0 @@ -use ouisync_lib::EntryType; - -pub const ENTRY_TYPE_FILE: u8 = 1; -pub const ENTRY_TYPE_DIRECTORY: u8 = 2; - -pub const ACCESS_MODE_BLIND: u8 = 0; -pub const ACCESS_MODE_READ: u8 = 1; -pub const ACCESS_MODE_WRITE: u8 = 2; - -pub(crate) fn entry_type_to_num(entry_type: EntryType) -> u8 { - match entry_type { - EntryType::File => ENTRY_TYPE_FILE, - EntryType::Directory => ENTRY_TYPE_DIRECTORY, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ouisync_lib::AccessMode; - - #[test] - fn access_mode_constants() { - for (mode, num) in [ - (AccessMode::Blind, ACCESS_MODE_BLIND), - (AccessMode::Read, ACCESS_MODE_READ), - (AccessMode::Write, ACCESS_MODE_WRITE), - ] { - assert_eq!(u8::from(mode), num); - assert_eq!(AccessMode::try_from(num).unwrap(), mode); - } - } -} diff --git a/ffi/src/directory.rs b/ffi/src/directory.rs index a1541fa45..1b76f14f1 100644 --- a/ffi/src/directory.rs +++ b/ffi/src/directory.rs @@ -1,4 +1,4 @@ -use crate::{constants, registry::Handle, repository::RepositoryHolder, state::State}; +use crate::{registry::Handle, repository::RepositoryHolder, state::State}; use camino::Utf8PathBuf; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ pub(crate) async fn open( .entries() .map(|entry| DirEntry { name: entry.unique_name().into_owned(), - entry_type: constants::entry_type_to_num(entry.entry_type()), + entry_type: entry.entry_type().into(), }) .collect(); diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 69b028851..0909116c6 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -2,7 +2,6 @@ #[macro_use] mod utils; -mod constants; mod dart; mod directory; mod error; @@ -17,10 +16,6 @@ mod state; mod state_monitor; mod transport; -pub use constants::{ - ACCESS_MODE_BLIND, ACCESS_MODE_READ, ACCESS_MODE_WRITE, ENTRY_TYPE_DIRECTORY, ENTRY_TYPE_FILE, -}; - use crate::{ dart::{Port, PortSender}, error::{ErrorCode, ToErrorCode}, @@ -32,6 +27,7 @@ use crate::{ #[cfg(unix)] use crate::{file::FileHolder, registry::Handle}; use bytes::Bytes; +use num_enum::{IntoPrimitive, TryFromPrimitive}; use ouisync_bridge::logger::{LogFormat, Logger}; use ouisync_lib::StateMonitor; #[cfg(unix)] @@ -246,23 +242,25 @@ pub unsafe extern "C" fn log_print( } }; - match level { - LOG_LEVEL_ERROR => tracing::error!("{}", message), - LOG_LEVEL_WARN => tracing::warn!("{}", message), - LOG_LEVEL_INFO => tracing::info!("{}", message), - LOG_LEVEL_DEBUG => tracing::debug!("{}", message), - LOG_LEVEL_TRACE => tracing::trace!("{}", message), - _ => { - tracing::error!(level, "invalid log level"); - } + match level.try_into() { + Ok(LogLevel::Error) => tracing::error!("{}", message), + Ok(LogLevel::Warn) => tracing::warn!("{}", message), + Ok(LogLevel::Info) => tracing::info!("{}", message), + Ok(LogLevel::Debug) => tracing::debug!("{}", message), + Ok(LogLevel::Trace) => tracing::trace!("{}", message), + Err(_) => tracing::error!(level, "invalid log level"), } } -pub const LOG_LEVEL_ERROR: u8 = 1; -pub const LOG_LEVEL_WARN: u8 = 2; -pub const LOG_LEVEL_INFO: u8 = 3; -pub const LOG_LEVEL_DEBUG: u8 = 4; -pub const LOG_LEVEL_TRACE: u8 = 5; +#[derive(Copy, Clone, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] +#[repr(u8)] +pub enum LogLevel { + Error = 1, + Warn = 2, + Info = 3, + Debug = 4, + Trace = 5, +} pub struct Session { pub(crate) runtime: Runtime, diff --git a/ffi/src/repository.rs b/ffi/src/repository.rs index e069f8bca..33f552679 100644 --- a/ffi/src/repository.rs +++ b/ffi/src/repository.rs @@ -1,5 +1,4 @@ use crate::{ - constants, error::Error, registry::Handle, state::{State, SubscriptionHandle}, @@ -224,7 +223,7 @@ pub(crate) async fn entry_type( let holder = state.get_repository(handle); match holder.repository.lookup_type(path).await { - Ok(entry_type) => Ok(Some(constants::entry_type_to_num(entry_type))), + Ok(entry_type) => Ok(Some(entry_type.into())), Err(ouisync_lib::Error::EntryNotFound) => Ok(None), Err(error) => Err(error), } diff --git a/lib/src/directory/entry_type.rs b/lib/src/directory/entry_type.rs index d67617fc8..abe68fe4b 100644 --- a/lib/src/directory/entry_type.rs +++ b/lib/src/directory/entry_type.rs @@ -1,8 +1,23 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; use serde::{Deserialize, Serialize}; /// Type of filesystem entry. -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] +#[derive( + Clone, + Copy, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + Debug, + Deserialize, + Serialize, + IntoPrimitive, + TryFromPrimitive, +)] +#[repr(u8)] pub enum EntryType { - File, - Directory, + File = 1, + Directory = 2, } diff --git a/utils/stress-test/Cargo.toml b/utils/stress-test/Cargo.toml index 3a5863c73..ad60e2c3f 100644 --- a/utils/stress-test/Cargo.toml +++ b/utils/stress-test/Cargo.toml @@ -14,7 +14,7 @@ name = "stress-test" path = "src/main.rs" [dependencies] -clap = { version = "4.2.7", features = ["derive"] } +clap = { workspace = true } serde = { workspace = true } serde_json = "1.0.94" tempfile = "3.2"