1use std::ffi::CStr;
7
8pub fn build_offsets(row_count: u32, stream_rows: u32) -> Vec<u32> {
23 let stream_rows = stream_rows.max(1);
24 let chunks = row_count.div_ceil(stream_rows);
25 let mut offsets = Vec::with_capacity(chunks as usize + 1);
26
27 for c in 0..chunks {
28 offsets.push(c * stream_rows);
29 }
30 offsets.push(row_count);
31
32 offsets
33}
34
35pub(crate) fn ptr_to_string(x: *const std::os::raw::c_char) -> String {
40 if x.is_null() {
41 String::new()
42 } else {
43 let cstr = unsafe { CStr::from_ptr(x) };
45 String::from_utf8_lossy(cstr.to_bytes()).to_string()
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use std::ffi::CString;
54
55 #[test]
58 fn build_offsets_exact_division() {
59 let offsets = build_offsets(30, 10);
60 assert_eq!(offsets, vec![0, 10, 20, 30]);
61 }
62
63 #[test]
64 fn build_offsets_non_exact_division() {
65 let offsets = build_offsets(25, 10);
66 assert_eq!(offsets, vec![0, 10, 20, 25]);
67 }
68
69 #[test]
70 fn build_offsets_stream_exceeds_row_count() {
71 let offsets = build_offsets(5, 10);
72 assert_eq!(offsets, vec![0, 5]);
73 }
74
75 #[test]
76 fn build_offsets_single_row() {
77 let offsets = build_offsets(1, 10);
78 assert_eq!(offsets, vec![0, 1]);
79 }
80
81 #[test]
82 fn build_offsets_equal_stream_and_rows() {
83 let offsets = build_offsets(10, 10);
84 assert_eq!(offsets, vec![0, 10]);
85 }
86
87 #[test]
88 fn build_offsets_zero_rows() {
89 let offsets = build_offsets(0, 10);
90 assert_eq!(offsets, vec![0]);
91 assert_eq!(offsets.windows(2).count(), 0);
93 }
94
95 #[test]
96 fn build_offsets_zero_chunk_size_is_one() {
97 assert_eq!(build_offsets(4, 0), vec![0, 1, 2, 3, 4]);
98 }
99
100 #[test]
101 fn build_offsets_windows_produce_valid_pairs() {
102 let offsets = build_offsets(25, 10);
103 let pairs: Vec<_> = offsets.windows(2).map(|w| (w[0], w[1])).collect();
104 assert_eq!(pairs, vec![(0, 10), (10, 20), (20, 25)]);
105 }
106
107 #[test]
108 fn build_offsets_single_chunk_windows() {
109 let offsets = build_offsets(5, 10);
110 let pairs: Vec<_> = offsets.windows(2).map(|w| (w[0], w[1])).collect();
111 assert_eq!(pairs, vec![(0, 5)]);
112 }
113
114 #[test]
115 fn build_offsets_large_dataset() {
116 let offsets = build_offsets(100_000, 10_000);
117 assert_eq!(offsets.len(), 11);
118 assert_eq!(*offsets.first().unwrap(), 0);
119 assert_eq!(*offsets.last().unwrap(), 100_000);
120 }
121
122 #[test]
125 fn ptr_to_string_null_returns_empty() {
126 let result = ptr_to_string(std::ptr::null());
127 assert_eq!(result, "");
128 }
129
130 #[test]
131 fn ptr_to_string_valid_cstring() {
132 let cs = CString::new("hello").unwrap();
133 let result = ptr_to_string(cs.as_ptr());
134 assert_eq!(result, "hello");
135 }
136
137 #[test]
138 fn ptr_to_string_empty_cstring() {
139 let cs = CString::new("").unwrap();
140 let result = ptr_to_string(cs.as_ptr());
141 assert_eq!(result, "");
142 }
143
144 #[test]
145 fn ptr_to_string_with_unicode() {
146 let cs = CString::new("UTF-8 encoded: café").unwrap();
147 let result = ptr_to_string(cs.as_ptr());
148 assert_eq!(result, "UTF-8 encoded: café");
149 }
150
151 #[test]
152 fn ptr_to_string_with_truncated_utf8() {
153 let mut buf = b"caf\xC3".to_vec();
160 buf.push(0); let ptr = buf.as_ptr().cast::<std::os::raw::c_char>();
162
163 let result = ptr_to_string(ptr);
164 assert_eq!(result, "caf\u{FFFD}");
165 }
166
167 #[test]
168 fn ptr_to_string_with_invalid_continuation_byte() {
169 let mut buf = b"hello\xFFworld".to_vec();
171 buf.push(0);
172 let ptr = buf.as_ptr().cast::<std::os::raw::c_char>();
173
174 let result = ptr_to_string(ptr);
175 assert_eq!(result, "hello\u{FFFD}world");
176 }
177
178 mod property_tests {
181 use super::*;
182 use proptest::prelude::*;
183
184 proptest! {
185 #[test]
187 fn offsets_start_at_zero_end_at_row_count(
188 row_count in 0u32..100_000,
189 stream_rows in 0u32..50_000
190 ) {
191 let offsets = build_offsets(row_count, stream_rows);
192 prop_assert_eq!(*offsets.first().unwrap(), 0);
193 prop_assert_eq!(*offsets.last().unwrap(), row_count);
194 }
195
196 #[test]
198 fn offsets_are_monotonically_increasing(
199 row_count in 1u32..100_000,
200 stream_rows in 0u32..50_000
201 ) {
202 let offsets = build_offsets(row_count, stream_rows);
203 for pair in offsets.windows(2) {
204 prop_assert!(pair[0] < pair[1], "offsets not strictly increasing: {} >= {}", pair[0], pair[1]);
205 prop_assert!(pair[1] <= row_count);
206 }
207 }
208
209 #[test]
211 fn chunk_sizes_bounded_by_stream_rows(
212 row_count in 1u32..100_000,
213 stream_rows in 1u32..50_000
214 ) {
215 let offsets = build_offsets(row_count, stream_rows);
216 for pair in offsets.windows(2) {
217 let chunk_size = pair[1] - pair[0];
218 prop_assert!(chunk_size <= stream_rows, "chunk {} > stream_rows {}", chunk_size, stream_rows);
219 }
220 }
221
222 #[test]
224 fn chunks_cover_all_rows(
225 row_count in 0u32..100_000,
226 stream_rows in 1u32..50_000
227 ) {
228 let offsets = build_offsets(row_count, stream_rows);
229 let total: u32 = offsets.windows(2).map(|w| w[1] - w[0]).sum();
230 prop_assert_eq!(total, row_count);
231 }
232
233 #[test]
235 fn zero_stream_rows_does_not_panic(row_count in 0u32..10_000) {
236 let offsets = build_offsets(row_count, 0);
237 prop_assert_eq!(*offsets.first().unwrap(), 0);
238 prop_assert_eq!(*offsets.last().unwrap(), row_count);
239 }
240 }
241 }
242}