1use std::ffi::CString;
14use std::os::raw::c_char;
15
16#[repr(C)]
22pub struct PluginMetadata {
23 pub name: *const c_char,
24 pub version: *const c_char,
25 pub vendor: *const c_char,
26 pub license: *const c_char,
27 pub homepage_url: *const c_char,
28 pub source_url: *const c_char,
29 pub issue_tracker_url: *const c_char,
30 pub description: *const c_char,
31}
32
33unsafe impl Sync for PluginMetadata {}
39unsafe impl Send for PluginMetadata {}
40
41#[derive(Default)]
49pub struct PluginMetadataBuilder {
50 name: Option<CString>,
51 version: Option<CString>,
52 vendor: Option<CString>,
53 license: Option<CString>,
54 homepage_url: Option<CString>,
55 source_url: Option<CString>,
56 issue_tracker_url: Option<CString>,
57 description: Option<CString>,
58}
59
60impl PluginMetadataBuilder {
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn name(mut self, s: impl Into<String>) -> Self {
66 self.name = CString::new(s.into()).ok();
67 self
68 }
69 pub fn version(mut self, s: impl Into<String>) -> Self {
70 self.version = CString::new(s.into()).ok();
71 self
72 }
73 pub fn vendor(mut self, s: impl Into<String>) -> Self {
74 self.vendor = CString::new(s.into()).ok();
75 self
76 }
77 pub fn license(mut self, s: impl Into<String>) -> Self {
78 self.license = CString::new(s.into()).ok();
79 self
80 }
81 pub fn homepage_url(mut self, s: impl Into<String>) -> Self {
82 self.homepage_url = CString::new(s.into()).ok();
83 self
84 }
85 pub fn source_url(mut self, s: impl Into<String>) -> Self {
86 self.source_url = CString::new(s.into()).ok();
87 self
88 }
89 pub fn issue_tracker_url(mut self, s: impl Into<String>) -> Self {
90 self.issue_tracker_url = CString::new(s.into()).ok();
91 self
92 }
93 pub fn description(mut self, s: impl Into<String>) -> Self {
94 self.description = CString::new(s.into()).ok();
95 self
96 }
97
98 pub fn build(self) -> PluginMetadata {
103 PluginMetadata {
104 name: self.name.map(leak_cstring).unwrap_or(std::ptr::null()),
105 version: self.version.map(leak_cstring).unwrap_or(std::ptr::null()),
106 vendor: self.vendor.map(leak_cstring).unwrap_or(std::ptr::null()),
107 license: self.license.map(leak_cstring).unwrap_or(std::ptr::null()),
108 homepage_url: self
109 .homepage_url
110 .map(leak_cstring)
111 .unwrap_or(std::ptr::null()),
112 source_url: self
113 .source_url
114 .map(leak_cstring)
115 .unwrap_or(std::ptr::null()),
116 issue_tracker_url: self
117 .issue_tracker_url
118 .map(leak_cstring)
119 .unwrap_or(std::ptr::null()),
120 description: self
121 .description
122 .map(leak_cstring)
123 .unwrap_or(std::ptr::null()),
124 }
125 }
126}
127
128fn leak_cstring(s: CString) -> *const c_char {
131 let ptr = s.as_ptr();
132 std::mem::forget(s);
133 ptr
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::ffi::CStr;
140
141 #[test]
142 fn builder_yields_nul_terminated_strings() {
143 let md = PluginMetadataBuilder::new()
144 .name("mock-hash")
145 .version("0.1.0")
146 .vendor("confium")
147 .license("BSD-2-Clause")
148 .description("deterministic mock hash for tests")
149 .build();
150
151 unsafe {
152 assert_eq!(CStr::from_ptr(md.name).to_str().unwrap(), "mock-hash");
153 assert_eq!(CStr::from_ptr(md.version).to_str().unwrap(), "0.1.0");
154 assert_eq!(CStr::from_ptr(md.vendor).to_str().unwrap(), "confium");
155 assert_eq!(CStr::from_ptr(md.license).to_str().unwrap(), "BSD-2-Clause");
156 assert_eq!(
157 CStr::from_ptr(md.description).to_str().unwrap(),
158 "deterministic mock hash for tests"
159 );
160 }
161 assert!(md.homepage_url.is_null());
162 assert!(md.source_url.is_null());
163 assert!(md.issue_tracker_url.is_null());
164 }
165
166 #[test]
167 fn empty_builder_yields_all_null() {
168 let md = PluginMetadataBuilder::new().build();
169 assert!(md.name.is_null());
170 assert!(md.version.is_null());
171 assert!(md.vendor.is_null());
172 assert!(md.license.is_null());
173 assert!(md.homepage_url.is_null());
174 assert!(md.source_url.is_null());
175 assert!(md.issue_tracker_url.is_null());
176 assert!(md.description.is_null());
177 }
178}