nibb_core/ffi/
ffi.rs

1use std::ffi::{CStr, CString};
2use std::os::raw::c_char;
3use slug::slugify;
4use crate::ffi::ffi_utils::{c_str_from_str, load_repo_ffi, str_from_c_str};
5use crate::fs::get_nibb_dir;
6use crate::result::NibbFFIError;
7use crate::{FSRepo, Snippet, SnippetRepository};
8use crate::git::git_integration::nibb_git_generic;
9
10/// Loads a snippet by name and returns its JSON representation.
11///
12/// # Arguments
13/// - `name`: A null-terminated C string representing the name of the snippet.
14///
15/// # Returns
16/// A newly allocated C string (`*mut c_char`) containing the snippet's JSON representation.
17/// - On success: JSON-encoded `Snippet` as a C string (must be freed with `free_string_ffi`).
18/// - On failure: JSON-encoded error object (must also be freed).
19///
20/// # Safety
21/// - `name` must be a valid, null-terminated UTF-8 string.
22/// - Caller is responsible for freeing the returned string using `free_string_ffi`.
23#[unsafe(no_mangle)]
24pub extern "C" fn load_snippet_ffi(name: *const c_char) -> *mut c_char {
25    let repo = match load_repo_ffi() {
26        Ok(repo) => repo,
27        Err(e) => return e,
28    };
29
30    let name = unsafe { CStr::from_ptr(name) };
31    let name = match name.to_str() {
32        Ok(name) => name,
33        Err(e) => return c_str_from_str(&NibbFFIError::FFIError(e.to_string()).to_json())
34    };
35    let slug = slugify(name);
36    let snippet = match repo.load(&slug) {
37        Ok(snippet) => snippet,
38        Err(e) => return c_str_from_str(&e.to_json())
39    };
40    c_str_from_str(&snippet.to_json())
41}
42
43/// Saves a single snippet from its JSON representation.
44///
45/// # Arguments
46/// - `snippet_json`: A null-terminated C string containing a JSON-encoded snippet.
47///
48/// # Returns
49/// - `true` if the snippet was saved successfully.
50/// - `false` if the input was invalid or saving failed.
51///
52/// # Safety
53/// - `snippet_json` must be a valid, null-terminated UTF-8 string.
54#[unsafe(no_mangle)]
55pub extern "C" fn save_snippet_ffi(snippet_json: *const c_char) -> bool {
56    let snippet_json = str_from_c_str(snippet_json);
57    let snippet = match serde_json::from_str(&snippet_json) {
58        Ok(snippet) => {
59            snippet
60        }
61        Err(_) => {
62            return false;
63        }
64    };
65    let repo = match FSRepo::new(get_nibb_dir().unwrap()) {
66        Ok(repo) => repo,
67        Err(_) => return false
68    };
69    match repo.save(&snippet) {
70        Ok(_) => true,
71        Err(_) => false,
72    }
73}
74
75/// Deletes a snippet from the repo
76///
77/// # Arguments
78/// - `name`: A Null terminated C String of the snippets' name.
79///
80/// # Returns
81/// - `true` if the snippet was deleted successfully.
82/// - `false` if an error occurred.
83///
84/// # Safety
85/// - `name` needs to be a valid, null-terminated, UTF-8 string.
86#[unsafe(no_mangle)]
87pub extern "C" fn delete_snippet_ffi(name: *const c_char) -> bool {
88    let repo = match FSRepo::new(get_nibb_dir().unwrap()) {
89        Ok(repo) => repo,
90        Err(_) => return false
91    };
92    let name = str_from_c_str(name);
93    match repo.delete(&name) {
94        Ok(_) => true,
95        Err(_) => false,
96    }
97}
98
99/// Loads all snippets from the repository and returns them as a JSON array.
100///
101/// # Returns
102/// A newly allocated C string (`*mut c_char`) containing the JSON array of all snippets.
103/// - On success: JSON array of snippets (must be freed with `free_string_ffi`).
104/// - On failure: JSON-encoded error object (must also be freed).
105///
106/// # Safety
107/// - Caller is responsible for freeing the returned string using `free_string_ffi`.
108#[unsafe(no_mangle)]
109pub extern "C" fn load_all_ffi() -> *mut c_char {
110    let repo = match load_repo_ffi() {
111        Ok(repo) => repo,
112        Err(e) => return e,
113    };
114    let snippets = match repo.load_all() {
115        Ok(snippets) => snippets,
116        Err(e) => return c_str_from_str(&e.to_json())
117    };
118    c_str_from_str(
119        &serde_json::to_string(&snippets)
120            .unwrap_or_else(|_| "{\"type\":\"Other\",\"message\":\"Serialization failed\"}"
121                .to_string())
122    )
123}
124
125/// Saves a list of snippets from a JSON array.
126///
127/// # Arguments
128/// - `snippets_json`: A null-terminated C string containing a JSON array of snippets.
129///
130/// # Returns
131/// - `true` if all snippets were saved successfully.
132/// - `false` if deserialization or saving failed.
133///
134/// # Safety
135/// - `snippets_json` must be a valid, null-terminated UTF-8 string.
136#[unsafe(no_mangle)]
137pub extern "C" fn save_all_ffi(snippets_json: *const c_char) -> bool {
138    let snippets_json = str_from_c_str(snippets_json);
139    let snippets: Vec<Snippet> = match serde_json::from_str(&snippets_json) {
140        Ok(snippets) => {
141            snippets
142        }
143        Err(_) => {
144            return false;
145        }
146    };
147    let repo = match load_repo_ffi() {
148        Ok(repo) => repo,
149        Err(_) => return false,
150    };
151    match repo.save_all(&snippets) {
152        Ok(_) => true,
153        Err(_) => false,
154    }
155
156}
157
158/// Executes a generic Git command inside the `.nibb` directory and returns the output as JSON.
159///
160/// # Arguments
161/// - `args`: A pointer to a null-terminated C string containing the Git arguments as a whitespace-separated string, e.g. `"status -s"`.
162///
163/// # Returns
164/// - A pointer to a null-terminated C string containing a JSON object:
165///   ```json
166///   {
167///     "stdout": "<Git stdout output>",
168///     "stderr": "<Git stderr output>"
169///   }
170///   ```
171///   - In case of error, `stderr` contains an error message and `stdout` is empty.
172///
173/// # Safety
174/// - The input C string must be valid and null-terminated.
175/// - The returned string must be freed by the caller using the appropriate function (e.g., `free_string_ffi`).
176///
177/// # Example (from Lua)
178/// ```lua
179/// local output_json = ffi.C.nibb_git_generic_ffi("status -s")
180/// local output = vim.fn.json_decode(ffi.string(output_json))
181/// print(output.stdout)
182/// print(output.stderr)
183/// ```
184#[unsafe(no_mangle)]
185pub extern "C" fn nibb_git_generic_ffi(args: *const c_char) -> *const c_char{
186    let args = str_from_c_str(args);
187    let args: Vec<String> = args.split_whitespace().map(|s| s.to_string()).collect();
188    let out = nibb_git_generic(args).unwrap_or_else(|e| e.to_json());
189    c_str_from_str(&out)
190}
191
192/// Frees a string previously allocated and returned by an FFI function.
193///
194/// # Arguments
195/// - `s`: A pointer returned by an FFI function like `load_snippet_ffi` or `load_all_ffi`.
196///
197/// # Safety
198/// - `s` must be a pointer obtained from one of the FFI functions using `CString::into_raw`.
199/// - Passing a null pointer is safe and does nothing.
200/// - After calling this function, `s` must not be used again.
201#[unsafe(no_mangle)]
202pub extern "C" fn free_string_ffi(s: *mut c_char) {
203    unsafe {
204        if s.is_null() {
205            return;
206        }
207        drop(CString::from_raw(s));
208    }
209}