diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4eaa44b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Run tests +on: [push, pull_request] + +jobs: + test_macos: + runs-on: macos-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install libeditorconfig + run: brew install editorconfig + - name: Run tests + run: cargo test + test_ubuntu: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Install libeditorconfig + run: sudo apt-get update && sudo apt-get install libeditorconfig-dev + - name: Run tests + run: cargo test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b752b6a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "editorconfig-sys" +version = "0.1.0" +edition = "2021" +authors = ["Thorsten Blum "] +homepage = "https://github.com/toblux/editorconfig-sys" +repository = "https://github.com/toblux/editorconfig-sys" +documentation = "https://docs.rs/editorconfig-sys" +description = "Native Rust bindings to libeditorconfig" +readme = "README.md" +license = "MIT" +keywords = ["editorconfig", "libeditorconfig", "bindings", "ffi", "sys"] +categories = ["external-ffi-bindings"] +links = "editorconfig" +build = "build.rs" + +[dev-dependencies] +rand = "0.8.5" + +[build-dependencies] +bindgen = "0.69.2" +pkg-config = "0.3.29" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..793aeb7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Thorsten Blum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fff40d0 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Native Rust Bindings to libeditorconfig + +This crate uses [bindgen](https://crates.io/crates/bindgen) and [pkg-config](https://crates.io/crates/pkg-config) to automatically generate Rust FFI bindings to the [EditorConfig Core C](https://github.com/editorconfig/editorconfig-core-c) library. + +Following the `*-sys` package convention, `editorconfig-sys` is just a thin wrapper around the native `libeditorconfig` library. + + + +![Workflow status](https://github.com/toblux/editorconfig-sys/actions/workflows/test.yml/badge.svg) + +## Dependencies + +To use this crate, `libeditorconfig >= 0.12.5` must be installed and `pkg-config` must be able to find it. You can check if `pkg-config` can find the library and which version is installed with: + +```sh +pkg-config --modversion editorconfig +``` + +## Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +editorconfig-sys = "0.1.0" +``` + +## Usage + +Some `unsafe` Rust code examples can be found in the [tests](tests/editorconfig_sys.rs). diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..bf099ce --- /dev/null +++ b/build.rs @@ -0,0 +1,37 @@ +use pkg_config::Library; + +const LIBRARY_NAME: &str = "editorconfig"; + +// Technically libeditorconfig v0.12.2 already supports pkg-config: +// https://github.com/editorconfig/editorconfig-core-c/releases/tag/v0.12.2 +const MIN_VERSION: &str = "0.12.5"; +const MAX_VERSION: &str = "1.0.0"; + +fn main() { + let err_msg = format!("Unable to find library {} >= {}", LIBRARY_NAME, MIN_VERSION); + let lib = pkg_config::Config::new() + .range_version(MIN_VERSION..MAX_VERSION) + .probe(LIBRARY_NAME) + .expect(&err_msg); + gen_bindings(lib); +} + +fn gen_bindings(lib: Library) { + let include_paths = lib + .include_paths + .iter() + .map(|path| format!("-I{}", path.to_string_lossy())); + + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .clang_args(include_paths) + .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) + .generate() + .expect("Failed to generate bindings"); + + // Write bindings to `$OUT_DIR/bindings.rs` + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Failed to write bindings"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0ec680f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +#![allow(non_camel_case_types)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..d71ad83 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf + +[*.rs] +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/tests/.editorconfig.invalid b/tests/.editorconfig.invalid new file mode 100644 index 0000000..b9340fd --- /dev/null +++ b/tests/.editorconfig.invalid @@ -0,0 +1,3 @@ +root = true +# Comments are fine +LINE 3 IS INVALID AND SHOULD CAUSE AN ERROR diff --git a/tests/editorconfig_sys.rs b/tests/editorconfig_sys.rs new file mode 100644 index 0000000..c7fe39f --- /dev/null +++ b/tests/editorconfig_sys.rs @@ -0,0 +1,312 @@ +use editorconfig_sys::*; +use rand::Rng; +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + fs, + path::Path, + ptr, +}; + +const DEFAULT_CONF_FILE_NAME: &str = ".editorconfig"; + +#[test] +fn create_destroy_handle() { + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn get_error_file() { + let invalid_conf_file_path = + fs::canonicalize(Path::new("tests/.editorconfig.invalid")).unwrap(); + let invalid_conf_file_name = invalid_conf_file_path.file_name().unwrap(); + let invalid_conf_file_name = invalid_conf_file_name.to_str().unwrap(); + let invalid_conf_file_name = CString::new(invalid_conf_file_name).unwrap(); + + // We use this .rs file for testing, but this could be any file as we are + // only interested in the errors from an invalid config file when parsing it + let test_file_path = fs::canonicalize(file!()).unwrap(); + let test_file_path = test_file_path.to_str().unwrap(); + let test_file_path = CString::new(test_file_path).unwrap(); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + // Parse test file with the default and valid config file + let err_num = editorconfig_parse(test_file_path.as_ptr(), h); + assert_eq!(err_num, 0); + + // No error, no error file + let err_file = editorconfig_handle_get_err_file(h); + assert!(err_file.is_null()); + + // Set invalid config file name + editorconfig_handle_set_conf_file_name(h, invalid_conf_file_name.as_ptr()); + + // Parse test file with an invalid config file + let err_num = editorconfig_parse(test_file_path.as_ptr(), h); + assert_eq!(err_num, 3, "Error at line 3 in invalid config file"); + + let err_file_path = editorconfig_handle_get_err_file(h); + assert!(!err_file_path.is_null()); + + let err_file_path = CStr::from_ptr(err_file_path).to_str().unwrap(); + assert_eq!(err_file_path, invalid_conf_file_path.to_str().unwrap()); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn get_version() { + let mut major = -1; + let mut minor = -1; + let mut patch = -1; + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + editorconfig_handle_get_version(h, &mut major, &mut minor, &mut patch); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } + + assert_eq!(major, 0); + assert_eq!(minor, 0); + assert_eq!(patch, 0); +} + +#[test] +fn set_get_version() { + let mut rng = rand::thread_rng(); + + let mut out_major = -1; + let mut out_minor = -1; + let mut out_patch = -1; + + for _ in 1..1000 { + let in_major = rng.gen_range(0..1000); + let in_minor = rng.gen_range(1..1000); + let in_patch = rng.gen_range(0..1000); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + editorconfig_handle_set_version(h, in_major, in_minor, in_patch); + editorconfig_handle_get_version(h, &mut out_major, &mut out_minor, &mut out_patch); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } + + assert_eq!(in_major, out_major); + assert_eq!(in_minor, out_minor); + assert_eq!(in_patch, out_patch); + } +} + +#[test] +fn get_conf_file_name() { + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + let conf_file_name = editorconfig_handle_get_conf_file_name(h); + assert!(conf_file_name.is_null()); + } +} + +#[test] +fn set_get_conf_file_name() { + let conf_file_name = CString::new(DEFAULT_CONF_FILE_NAME).unwrap(); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + editorconfig_handle_set_conf_file_name(h, conf_file_name.as_ptr()); + + let conf_file_name = editorconfig_handle_get_conf_file_name(h); + assert!(!conf_file_name.is_null()); + + let conf_file_name = CStr::from_ptr(conf_file_name).to_str().unwrap(); + assert_eq!(conf_file_name, DEFAULT_CONF_FILE_NAME); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn parse_config_file_and_determine_rules_for_rust_file() { + // As defined in .editorconfig + let mut rs_file_rules = HashMap::new(); + rs_file_rules.insert("charset", "utf-8"); + rs_file_rules.insert("end_of_line", "lf"); + rs_file_rules.insert("insert_final_newline", "true"); + rs_file_rules.insert("trim_trailing_whitespace", "true"); + + // We use this .rs file for testing, but libeditorconfig requires absolute paths + let test_file_path = fs::canonicalize(file!()).unwrap(); + let test_file_path = test_file_path.to_str().unwrap(); + let test_file_path = CString::new(test_file_path).unwrap(); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + let err_num = editorconfig_parse(test_file_path.as_ptr(), h); + assert_eq!(err_num, 0); + + let rule_count = editorconfig_handle_get_name_value_count(h); + assert_eq!(rule_count as usize, rs_file_rules.len()); + + let (mut rule_name, mut rule_value) = (ptr::null(), ptr::null()); + + for rule_index in 0..rule_count { + editorconfig_handle_get_name_value(h, rule_index, &mut rule_name, &mut rule_value); + + assert!(!rule_name.is_null()); + assert!(!rule_value.is_null()); + + let rule_name = CStr::from_ptr(rule_name).to_str().unwrap(); + let rule_value = CStr::from_ptr(rule_value).to_str().unwrap(); + assert_eq!(rs_file_rules.get(rule_name).unwrap(), &rule_value); + } + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn no_parse_get_rule_count() { + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + let rule_count = editorconfig_handle_get_name_value_count(h); + assert_eq!(rule_count, 0); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn relative_file_path_error() { + let relative_file_path = CString::new(file!()).unwrap(); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + // Compare error number with error constant + let err_num = editorconfig_parse(relative_file_path.as_ptr(), h); + assert_eq!(err_num, EDITORCONFIG_PARSE_NOT_FULL_PATH); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn version_too_new_error() { + let test_file_path = fs::canonicalize(file!()).unwrap(); + let test_file_path = test_file_path.to_str().unwrap(); + let test_file_path = CString::new(test_file_path).unwrap(); + + unsafe { + let h = editorconfig_handle_init(); + assert!(!h.is_null()); + + editorconfig_handle_set_version(h, i32::MAX, i32::MAX, i32::MAX); + + // Compare error number with error constant + let err_num = editorconfig_parse(test_file_path.as_ptr(), h); + assert_eq!(err_num, EDITORCONFIG_PARSE_VERSION_TOO_NEW); + + assert_eq!(editorconfig_handle_destroy(h), 0); + } +} + +#[test] +fn get_error_message_no_error() { + let no_err_num = 0; + + unsafe { + let err_msg = editorconfig_get_error_msg(no_err_num); + assert!(!err_msg.is_null()); + + let err_msg = CStr::from_ptr(err_msg).to_str().unwrap(); + assert!(err_msg.len() > 0); + } +} + +#[test] +fn get_error_message_parse_error() { + let mut rng = rand::thread_rng(); + + // Any error > 0 is a parsing error at that line + let parse_err_line_num = rng.gen_range(1..=i32::MAX); + + unsafe { + let err_msg = editorconfig_get_error_msg(parse_err_line_num); + assert!(!err_msg.is_null()); + + let err_msg = CStr::from_ptr(err_msg).to_str().unwrap(); + assert!(err_msg.len() > 0); + } +} + +#[test] +fn get_error_message_relative_path_error() { + unsafe { + let err_msg = editorconfig_get_error_msg(EDITORCONFIG_PARSE_NOT_FULL_PATH); + assert!(!err_msg.is_null()); + + let err_msg = CStr::from_ptr(err_msg).to_str().unwrap(); + assert!(err_msg.len() > 0); + } +} + +#[test] +fn get_error_message_memory_error() { + unsafe { + let err_msg = editorconfig_get_error_msg(EDITORCONFIG_PARSE_MEMORY_ERROR); + assert!(!err_msg.is_null()); + + let err_msg = CStr::from_ptr(err_msg).to_str().unwrap(); + assert!(err_msg.len() > 0); + } +} + +#[test] +fn get_error_message_version_error() { + unsafe { + let err_msg = editorconfig_get_error_msg(EDITORCONFIG_PARSE_VERSION_TOO_NEW); + assert!(!err_msg.is_null()); + + let err_msg = CStr::from_ptr(err_msg).to_str().unwrap(); + assert!(err_msg.len() > 0); + } +} + +#[test] +fn lib_get_version() { + let mut major = -1; + let mut minor = -1; + let mut patch = -1; + + unsafe { + editorconfig_get_version(&mut major, &mut minor, &mut patch); + } + + // libeditorconfig 0.12.5 is currently the minimum supported version + assert!(major >= 0); + assert!(minor >= 12); + assert!(patch >= 5); +} diff --git a/wrapper.h b/wrapper.h new file mode 100644 index 0000000..08d106b --- /dev/null +++ b/wrapper.h @@ -0,0 +1 @@ +#include