1pub mod csv;
6pub mod json;
7pub mod logstream;
8pub mod plaintext;
9pub mod sql;
10
11pub use csv::{mask_csv_reader, scan_csv_str, CsvCellScanResult, CsvScanOptions, CsvScanResult};
12pub use json::{
13 scan_json_str, scan_json_value, JsonScanOptions, JsonScanResult, JsonStringScanResult,
14};
15pub use logstream::{
16 mask_log_reader, scan_log_str, LogLineFormat, LogLineScanResult, LogStreamScanResult,
17};
18pub use plaintext::{scan_lines, scan_text, LineScanResult};
19pub use sql::{mask_sql_reader, scan_sql_str, SqlScanResult, SqlValueScanResult};
20
21use cloakrs_core::{PiiEntity, Result, Scanner};
22use serde::{Deserialize, Serialize};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum AdapterKind {
27 Plaintext,
29 Json,
31 Csv,
33 LogStream,
35 Sql,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct AdapterFinding {
42 pub location: String,
44 pub findings: Vec<PiiEntity>,
46 pub masked_value: Option<String>,
48}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct AdapterReport {
53 pub kind: AdapterKind,
55 pub findings: Vec<AdapterFinding>,
57 pub masked_output: String,
59}
60
61pub trait Adapter {
90 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport>;
92}
93
94#[derive(Debug, Default, Clone, Copy)]
96pub struct PlaintextAdapter;
97
98#[derive(Debug, Default, Clone)]
100pub struct JsonAdapter {
101 pub options: JsonScanOptions,
103}
104
105#[derive(Debug, Default, Clone)]
107pub struct CsvAdapter {
108 pub options: CsvScanOptions,
110}
111
112#[derive(Debug, Default, Clone, Copy)]
114pub struct LogStreamAdapter;
115
116#[derive(Debug, Default, Clone, Copy)]
118pub struct SqlAdapter;
119
120impl Adapter for PlaintextAdapter {
121 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport> {
122 let lines = scan_text(input, scanner)?;
123 let findings = lines
124 .iter()
125 .filter(|line| !line.findings.is_empty())
126 .map(|line| AdapterFinding {
127 location: format!("line:{}", line.line_number),
128 findings: line.findings.clone(),
129 masked_value: line.masked_line.clone(),
130 })
131 .collect();
132 let masked_output = masked_plaintext_output(input, &lines);
133 Ok(AdapterReport {
134 kind: AdapterKind::Plaintext,
135 findings,
136 masked_output,
137 })
138 }
139}
140
141fn masked_plaintext_output(input: &str, lines: &[LineScanResult]) -> String {
142 let mut output = String::with_capacity(input.len());
143 for (index, segment) in input.split_inclusive('\n').enumerate() {
144 let line = segment.strip_suffix('\n').unwrap_or(segment);
145 let line = line.strip_suffix('\r').unwrap_or(line);
146 let masked = lines
147 .get(index)
148 .and_then(|result| result.masked_line.as_deref())
149 .unwrap_or(line);
150 output.push_str(masked);
151 if segment.ends_with('\n') {
152 if segment.ends_with("\r\n") {
153 output.push('\r');
154 }
155 output.push('\n');
156 }
157 }
158 if !input.contains('\n') {
159 return lines
160 .first()
161 .and_then(|result| result.masked_line.clone())
162 .unwrap_or_else(|| input.to_string());
163 }
164 output
165}
166
167impl Adapter for JsonAdapter {
168 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport> {
169 let result = scan_json_str(input, scanner, &self.options)?;
170 let findings = result
171 .strings
172 .into_iter()
173 .map(|string| AdapterFinding {
174 location: string.path,
175 findings: string.findings,
176 masked_value: string.masked_value,
177 })
178 .collect();
179 Ok(AdapterReport {
180 kind: AdapterKind::Json,
181 findings,
182 masked_output: serde_json::to_string(&result.masked_json)?,
183 })
184 }
185}
186
187impl Adapter for CsvAdapter {
188 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport> {
189 let result = scan_csv_str(input, scanner, &self.options)?;
190 let findings = result
191 .cells
192 .into_iter()
193 .map(|cell| AdapterFinding {
194 location: format!("row:{},column:{}", cell.row_number, cell.column_index),
195 findings: cell.findings,
196 masked_value: cell.masked_value,
197 })
198 .collect();
199 Ok(AdapterReport {
200 kind: AdapterKind::Csv,
201 findings,
202 masked_output: result.masked_csv,
203 })
204 }
205}
206
207impl Adapter for LogStreamAdapter {
208 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport> {
209 let result = scan_log_str(input, scanner)?;
210 let findings = result
211 .lines
212 .into_iter()
213 .filter(|line| !line.findings.is_empty())
214 .map(|line| AdapterFinding {
215 location: format!("line:{}", line.line_number),
216 findings: line.findings,
217 masked_value: line.masked_line,
218 })
219 .collect();
220 Ok(AdapterReport {
221 kind: AdapterKind::LogStream,
222 findings,
223 masked_output: result.masked_log,
224 })
225 }
226}
227
228impl Adapter for SqlAdapter {
229 fn scan_str(&self, input: &str, scanner: &Scanner) -> Result<AdapterReport> {
230 let result = scan_sql_str(input, scanner)?;
231 let findings = result
232 .values
233 .into_iter()
234 .map(|value| AdapterFinding {
235 location: format!(
236 "statement:{},value:{}",
237 value.statement_number, value.value_index
238 ),
239 findings: value.findings,
240 masked_value: value.masked_value,
241 })
242 .collect();
243 Ok(AdapterReport {
244 kind: AdapterKind::Sql,
245 findings,
246 masked_output: result.masked_sql,
247 })
248 }
249}
250
251#[must_use]
253pub fn version() -> &'static str {
254 env!("CARGO_PKG_VERSION")
255}