1use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct BulkJsonPayload {
11 pub collection: String,
12 pub json_payloads: Vec<String>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum BulkJsonError {
17 PayloadTooShort,
18 MissingCollectionLength,
19 TruncatedCollectionName,
20 InvalidCollectionName,
21 MissingRowCount,
22 MissingJsonLength,
23 TruncatedJsonPayload,
24 InvalidJsonPayload,
25 FieldTooLarge(&'static str),
26 RowCountOverflow,
27}
28
29impl fmt::Display for BulkJsonError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::PayloadTooShort => write!(f, "bulk insert: payload too short"),
33 Self::MissingCollectionLength => {
34 write!(f, "bulk insert: missing collection length")
35 }
36 Self::TruncatedCollectionName => {
37 write!(f, "bulk insert: truncated collection name")
38 }
39 Self::InvalidCollectionName => write!(f, "bulk insert: invalid collection name"),
40 Self::MissingRowCount => write!(f, "bulk insert: missing row count"),
41 Self::MissingJsonLength => write!(f, "bulk insert: missing JSON length"),
42 Self::TruncatedJsonPayload => write!(f, "bulk insert: truncated JSON payload"),
43 Self::InvalidJsonPayload => write!(f, "bulk insert: invalid JSON payload"),
44 Self::FieldTooLarge(field) => {
45 write!(f, "{field} is too large for RedWire JSON bulk insert")
46 }
47 Self::RowCountOverflow => {
48 write!(f, "row count is too large for RedWire JSON bulk insert")
49 }
50 }
51 }
52}
53
54impl std::error::Error for BulkJsonError {}
55
56pub fn encode_bulk_json_payload(
57 collection: &str,
58 json_payloads: &[String],
59) -> Result<Vec<u8>, BulkJsonError> {
60 let collection_len =
61 u16::try_from(collection.len()).map_err(|_| BulkJsonError::FieldTooLarge("collection"))?;
62 let row_count =
63 u32::try_from(json_payloads.len()).map_err(|_| BulkJsonError::RowCountOverflow)?;
64
65 let mut out = Vec::new();
66 out.extend_from_slice(&collection_len.to_le_bytes());
67 out.extend_from_slice(collection.as_bytes());
68 out.extend_from_slice(&row_count.to_le_bytes());
69 for payload in json_payloads {
70 let json_len =
71 u32::try_from(payload.len()).map_err(|_| BulkJsonError::FieldTooLarge("json"))?;
72 out.extend_from_slice(&json_len.to_le_bytes());
73 out.extend_from_slice(payload.as_bytes());
74 }
75 Ok(out)
76}
77
78pub fn decode_bulk_json_payload(payload: &[u8]) -> Result<BulkJsonPayload, BulkJsonError> {
79 if payload.len() < 2 {
80 return Err(BulkJsonError::PayloadTooShort);
81 }
82
83 let mut pos = 0;
84 let coll_len = read_u16(payload, &mut pos, BulkJsonError::MissingCollectionLength)? as usize;
85 let collection = read_string(
86 payload,
87 &mut pos,
88 coll_len,
89 BulkJsonError::TruncatedCollectionName,
90 BulkJsonError::InvalidCollectionName,
91 )?;
92
93 let nrows = read_u32(payload, &mut pos, BulkJsonError::MissingRowCount)? as usize;
94 let remaining = payload.len().saturating_sub(pos);
98 if nrows > remaining / 4 {
99 return Err(BulkJsonError::RowCountOverflow);
100 }
101 let mut json_payloads = Vec::with_capacity(nrows);
102 for _ in 0..nrows {
103 let json_len = read_u32(payload, &mut pos, BulkJsonError::MissingJsonLength)? as usize;
104 json_payloads.push(read_string(
105 payload,
106 &mut pos,
107 json_len,
108 BulkJsonError::TruncatedJsonPayload,
109 BulkJsonError::InvalidJsonPayload,
110 )?);
111 }
112
113 Ok(BulkJsonPayload {
114 collection,
115 json_payloads,
116 })
117}
118
119fn read_u16(payload: &[u8], pos: &mut usize, err: BulkJsonError) -> Result<u16, BulkJsonError> {
120 let bytes = read_bytes(payload, pos, 2, err)?;
121 Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
122}
123
124fn read_u32(payload: &[u8], pos: &mut usize, err: BulkJsonError) -> Result<u32, BulkJsonError> {
125 let bytes = read_bytes(payload, pos, 4, err)?;
126 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
127}
128
129fn read_string(
130 payload: &[u8],
131 pos: &mut usize,
132 len: usize,
133 truncated_err: BulkJsonError,
134 utf8_err: BulkJsonError,
135) -> Result<String, BulkJsonError> {
136 let bytes = read_bytes(payload, pos, len, truncated_err)?;
137 std::str::from_utf8(bytes)
138 .map(str::to_string)
139 .map_err(|_| utf8_err)
140}
141
142fn read_bytes<'a>(
143 payload: &'a [u8],
144 pos: &mut usize,
145 len: usize,
146 err: BulkJsonError,
147) -> Result<&'a [u8], BulkJsonError> {
148 let Some(end) = pos.checked_add(len) else {
149 return Err(err);
150 };
151 if end > payload.len() {
152 return Err(err);
153 }
154 let bytes = &payload[*pos..end];
155 *pos = end;
156 Ok(bytes)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn bulk_json_payload_round_trips() {
165 let rows = vec![r#"{"id":1}"#.to_string(), r#"{"id":2}"#.to_string()];
166 let bytes = encode_bulk_json_payload("events", &rows).unwrap();
167 let decoded = decode_bulk_json_payload(&bytes).unwrap();
168 assert_eq!(decoded.collection, "events");
169 assert_eq!(decoded.json_payloads, rows);
170 }
171
172 #[test]
173 fn bulk_json_decode_preserves_error_prefixes() {
174 assert_eq!(
175 decode_bulk_json_payload(&[0]).unwrap_err().to_string(),
176 "bulk insert: payload too short"
177 );
178
179 let payload = vec![1, 0, b't', 1, 0, 0, 0, 10, 0, 0, 0, b'{'];
180 assert_eq!(
181 decode_bulk_json_payload(&payload).unwrap_err().to_string(),
182 "bulk insert: truncated JSON payload"
183 );
184 }
185}