appcore_filemaker/export/
csv.rs1use std::borrow::Cow;
14use std::collections::BTreeSet;
15use std::io::Write;
16
17use crate::{
18 Dataset, ErrorCode, ExportCapabilities, ExportLossReport, ExportOutcome, FileMakerError,
19 ResourceLimits, Result, TableSpec,
20};
21
22pub fn export_dataset_csv(
24 spec: &TableSpec,
25 dataset: &dyn Dataset,
26 limits: &ResourceLimits,
27 writer: &mut dyn Write,
28) -> Result<ExportOutcome> {
29 limits.validate()?;
30 spec.validate()?;
31 let mut bounded = BoundedWriter::new(writer, limits.max_output_bytes);
32 write_record(
33 &mut bounded,
34 spec.columns.iter().map(|column| column.header.as_str()),
35 )?;
36 spec.visit_bounded(dataset, &mut |_, row| {
37 write_record(
38 &mut bounded,
39 spec.columns.iter().map(|column| {
40 row.get(&column.field)
41 .map_or(Cow::Borrowed(""), display_value)
42 }),
43 )
44 })?;
45 Ok(ExportOutcome {
46 bytes_written: bounded.written,
47 loss_report: ExportLossReport::default(),
48 capabilities: BTreeSet::from([ExportCapabilities::Semantic]),
49 })
50}
51
52pub fn export_dataset_csv_bytes(
54 spec: &TableSpec,
55 dataset: &dyn Dataset,
56 limits: &ResourceLimits,
57) -> Result<(Vec<u8>, ExportOutcome)> {
58 let mut bytes = Vec::new();
59 let outcome = export_dataset_csv(spec, dataset, limits, &mut bytes)?;
60 Ok((bytes, outcome))
61}
62
63fn write_record<T, S>(writer: &mut BoundedWriter<'_>, values: T) -> Result<()>
64where
65 T: IntoIterator<Item = S>,
66 S: AsRef<str>,
67{
68 let mut first = true;
69 for value in values {
70 if !first {
71 writer.write_all(b",")?;
72 }
73 first = false;
74 let value = value.as_ref();
75 write_field(writer, value)?;
76 }
77 writer.write_all(b"\r\n")
78}
79
80fn write_field(writer: &mut BoundedWriter<'_>, value: &str) -> Result<()> {
81 if !value.contains([',', '"', '\r', '\n']) {
82 return writer.write_all(value.as_bytes());
83 }
84 writer.write_all(b"\"")?;
85 let bytes = value.as_bytes();
86 let mut start = 0;
87 for (index, _) in value.match_indices('"') {
88 writer.write_all(&bytes[start..index])?;
89 writer.write_all(b"\"\"")?;
90 start = index + 1;
91 }
92 writer.write_all(&bytes[start..])?;
93 writer.write_all(b"\"")
94}
95
96fn display_value(value: &crate::DataValue) -> Cow<'_, str> {
97 match value {
98 crate::DataValue::String(value)
99 | crate::DataValue::Date(value)
100 | crate::DataValue::DateTime(value) => Cow::Borrowed(value),
101 crate::DataValue::Array(_) => Cow::Borrowed("[array]"),
102 crate::DataValue::Object(_) => Cow::Borrowed("[object]"),
103 crate::DataValue::Null => Cow::Borrowed(""),
104 _ => Cow::Owned(value.display()),
105 }
106}
107
108struct BoundedWriter<'a> {
109 inner: &'a mut dyn Write,
110 limit: usize,
111 written: usize,
112}
113
114impl<'a> BoundedWriter<'a> {
115 const fn new(inner: &'a mut dyn Write, limit: usize) -> Self {
116 Self {
117 inner,
118 limit,
119 written: 0,
120 }
121 }
122
123 fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
124 let next = self
125 .written
126 .checked_add(bytes.len())
127 .ok_or_else(|| limit_error("CSV byte accounting overflow"))?;
128 if next > self.limit {
129 return Err(limit_error("CSV exceeds configured output limit"));
130 }
131 self.inner
132 .write_all(bytes)
133 .map_err(|error| FileMakerError::new(ErrorCode::ExportWrite, error.to_string()))?;
134 self.written = next;
135 Ok(())
136 }
137}
138
139fn limit_error(message: impl Into<String>) -> FileMakerError {
140 FileMakerError::new(ErrorCode::LimitExceeded, message)
141}