1use std::{
37 error::Error as StdError,
38 ffi::{CStr, CString, c_char},
39 fs,
40 path::Path,
41 sync::Arc,
42};
43
44use crate::{
45 core::{
46 error::{self, Error, ErrorCode, FACT_COLUMN_INDEX, Result},
47 table::{Table, TableView},
48 },
49 runtime::{Runtime, execution_resolver::ResolvedExecution},
50};
51
52pub mod parquet;
53pub mod source;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum KvikioCompatMode {
58 Auto,
60 On,
62 Off,
64}
65
66impl KvikioCompatMode {
67 const fn as_ffi(self) -> i32 {
68 match self {
69 Self::Auto => cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_AUTO as i32,
70 Self::On => cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_ON as i32,
71 Self::Off => cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_OFF as i32,
72 }
73 }
74
75 fn from_ffi(value: i32) -> Option<Self> {
76 match value {
77 value if value == cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_AUTO as i32 => Some(Self::Auto),
78 value if value == cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_ON as i32 => Some(Self::On),
79 value if value == cudf_sys::CUDF_IO_KVIKIO_COMPAT_MODE_OFF as i32 => Some(Self::Off),
80 _ => None,
81 }
82 }
83
84 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::Auto => "auto",
88 Self::On => "on",
89 Self::Off => "off",
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
100pub struct KvikioProcessConfig {
101 pub compat_mode: KvikioCompatMode,
103 pub thread_count: u32,
105 pub task_size_bytes: u64,
108}
109
110impl KvikioProcessConfig {
111 pub const DEFAULT: Self =
119 Self { compat_mode: KvikioCompatMode::On, thread_count: 64, task_size_bytes: 4 * 1024 * 1024 };
120
121 fn from_ffi(raw: &cudf_sys::cudf_io_kvikio_process_config_t) -> Option<Self> {
122 Some(Self {
123 compat_mode: KvikioCompatMode::from_ffi(raw.compat_mode)?,
124 thread_count: raw.thread_count,
125 task_size_bytes: raw.task_size_bytes,
126 })
127 }
128
129 const fn as_ffi(&self) -> cudf_sys::cudf_io_kvikio_process_config_t {
130 cudf_sys::cudf_io_kvikio_process_config_t {
131 compat_mode: self.compat_mode.as_ffi(),
132 thread_count: self.thread_count,
133 task_size_bytes: self.task_size_bytes,
134 }
135 }
136}
137
138const fn zeroed_kvikio_process_config() -> cudf_sys::cudf_io_kvikio_process_config_t {
139 cudf_sys::cudf_io_kvikio_process_config_t { compat_mode: 0, thread_count: 0, task_size_bytes: 0 }
140}
141
142pub fn configure_kvikio_process(device_ordinal: usize, defaults: &KvikioProcessConfig) -> Result<KvikioProcessConfig> {
155 const OPERATION: &str = "io::configure_kvikio_process";
156 let device_ordinal = i32::try_from(device_ordinal).map_err(|_| {
157 Error::new_with(ErrorCode::ForeignInvalidArgument, OPERATION, "device ordinal exceeds the CUDA device range")
158 .with_detail("device_ordinal", device_ordinal.to_string())
159 })?;
160 let raw_defaults = defaults.as_ffi();
161 let mut raw_effective = zeroed_kvikio_process_config();
162 let mut active_io = 0u64;
163 error::check_status_in_with_details(
164 unsafe {
167 cudf_sys::cudf_io_kvikio_configure_process(
168 device_ordinal,
169 &raw_defaults,
170 &mut raw_effective,
171 &mut active_io,
172 )
173 },
174 OPERATION,
175 [("resource", "kvikio_defaults".to_string())],
176 )?;
177 kvikio_configure_result(&raw_effective, active_io)
178}
179
180fn kvikio_configure_result(
181 raw_effective: &cudf_sys::cudf_io_kvikio_process_config_t,
182 active_io: u64,
183) -> Result<KvikioProcessConfig> {
184 const OPERATION: &str = "io::configure_kvikio_process";
185 if active_io != 0 {
186 return Err(kvikio_busy_error(
187 OPERATION,
188 "cannot configure KvikIO while I/O is active",
189 "kvikio_defaults",
190 active_io,
191 "stop and join all I/O workers before configuring KvikIO",
192 ));
193 }
194 KvikioProcessConfig::from_ffi(raw_effective).ok_or_else(|| {
195 Error::new_with(
196 ErrorCode::ForeignInvariantViolation,
197 OPERATION,
198 "KvikIO reported an unknown compatibility mode",
199 )
200 .with_detail("compat_mode", raw_effective.compat_mode.to_string())
201 })
202}
203
204pub fn kvikio_process_config() -> Option<KvikioProcessConfig> {
207 let mut raw = zeroed_kvikio_process_config();
208 let status = unsafe { cudf_sys::cudf_io_kvikio_process_config(&mut raw) };
210 (status == cudf_sys::CUDF_SUCCESS).then(|| KvikioProcessConfig::from_ffi(&raw)).flatten()
211}
212
213fn kvikio_busy_error(
214 operation: &'static str,
215 message: &'static str,
216 resource: &'static str,
217 active_io: u64,
218 remedy: &'static str,
219) -> Error {
220 Error::new_with(ErrorCode::IoBusy, operation, message)
221 .with_detail("resource", resource)
222 .with_detail("reason", "active_io")
223 .with_detail("active_io", active_io.to_string())
224 .with_detail("remedy", remedy)
225}
226
227pub fn drain_kvikio_bounce_buffer_pools() -> Result<u64> {
233 const OPERATION: &str = "io::drain_kvikio_bounce_buffer_pools";
234 let mut cleared_bytes = 0u64;
235 let mut active_io = 0u64;
236 error::check_status_in_with_details(
237 unsafe { cudf_sys::cudf_io_kvikio_drain_bounce_buffer_pools(&mut cleared_bytes, &mut active_io) },
239 OPERATION,
240 [("resource", "kvikio_bounce_buffer_pools".to_string())],
241 )?;
242 kvikio_drain_result(cleared_bytes, active_io)
243}
244
245fn kvikio_drain_result(cleared_bytes: u64, active_io: u64) -> Result<u64> {
246 if active_io == 0 {
247 return Ok(cleared_bytes);
248 }
249 Err(kvikio_busy_error(
250 "io::drain_kvikio_bounce_buffer_pools",
251 "cannot drain KvikIO bounce buffers while I/O is active",
252 "kvikio_bounce_buffer_pools",
253 active_io,
254 "stop and join all I/O workers before retrying bounce-buffer drain",
255 ))
256}
257
258pub fn gds_driver_available() -> Result<()> {
265 const OPERATION: &str = "io::gds_driver_available";
266 error::check_status_in_with_details(
267 unsafe { cudf_sys::cudf_gds_driver_available() },
270 OPERATION,
271 [("resource", "cufile_driver".to_string())],
272 )
273}
274
275pub struct TableReadOutput {
277 pub table: Table,
278 pub column_names: Option<Vec<String>>,
279}
280
281fn resolve_write_execution(
282 table: &TableView<'_>,
283 runtime: Option<&Runtime>,
284 operation: &'static str,
285) -> Result<ResolvedExecution> {
286 let write_domain = runtime
287 .map(|runtime| Arc::clone(runtime.execution_domain()))
288 .unwrap_or_else(|| Arc::clone(table.execution_domain()));
289 ResolvedExecution::with_output_domain(write_domain, [table.execution_domain()], operation)
290}
291
292fn map_path_io_error(
302 source: impl StdError + Send + Sync + 'static,
303 path: &Path,
304 operation: &'static str,
305 message: &'static str,
306) -> Error {
307 Error::new_with(ErrorCode::IoFailure, operation, message)
308 .with_detail("path", path.display().to_string())
309 .with_source(source)
310}
311
312fn validate_input_path(path: &Path, operation: &'static str) -> Result<()> {
313 if path.to_string_lossy().contains("://") {
314 return Ok(());
315 }
316
317 let metadata = fs::metadata(path)
318 .map_err(|source| map_path_io_error(source, path, operation, "failed to access input path"))?;
319
320 if metadata.is_file() {
321 return Ok(());
322 }
323
324 if metadata.is_dir() {
325 return Err(Error::new_with(ErrorCode::PathIsDirectory, operation, "input path is a directory")
326 .with_detail("path", path.display().to_string())
327 .with_detail("resource", "input_path")
328 .with_detail("reason", "path_is_directory")
329 .with_detail("remedy", "provide a regular file path instead of a directory"));
330 }
331
332 Err(Error::new_with(ErrorCode::IoFailure, operation, "input path is not a regular file")
333 .with_detail("path", path.display().to_string())
334 .with_detail("resource", "input_path")
335 .with_detail("reason", "path_not_regular_file")
336 .with_detail("remedy", "provide a regular file path"))
337}
338
339fn path_to_cstring(path: &Path, operation: &'static str) -> Result<CString> {
341 let path_str = path.to_str().ok_or_else(|| {
342 Error::new_with(ErrorCode::IoFailure, operation, "path is not valid UTF-8")
346 .with_detail("resource", "path")
347 .with_detail("reason", "invalid_utf8")
348 })?;
349 CString::new(path_str).map_err(|_| {
350 error::interior_null_byte_error(operation, "path", "path contains interior null byte")
351 .with_detail("path", path.to_string_lossy().into_owned())
352 })
353}
354
355unsafe fn collect_column_names(
356 names_ptr: *mut *mut c_char,
357 num_names: i32,
358 operation: &'static str,
359) -> Result<Vec<String>> {
360 if num_names < 0 {
361 return Err(error::foreign_invariant_violation(
362 operation,
363 "ffi returned negative column name count",
364 "column_name_count",
365 "negative_name_count",
366 "ensure the foreign reader reports a non-negative column name count",
367 )
368 .with_detail("column_name_count", num_names.to_string()));
369 }
370
371 if num_names > 0 && names_ptr.is_null() {
372 return Err(error::foreign_invariant_violation(
373 operation,
374 "ffi returned null column name array with positive count",
375 "column_names",
376 "null_name_array",
377 "ensure the foreign reader returns a name array whenever column_name_count is positive",
378 )
379 .with_detail("column_name_count", num_names.to_string()));
380 }
381
382 let mut names = Vec::with_capacity(num_names as usize);
383 for i in 0..num_names {
384 let name_ptr = unsafe { *names_ptr.add(i as usize) };
387 if name_ptr.is_null() {
388 return Err(error::foreign_invariant_violation(
389 operation,
390 "ffi returned null column name entry",
391 "column_name",
392 "null_name_entry",
393 "ensure the foreign reader returns one non-null name entry per reported column",
394 )
395 .with_detail("column_name_count", num_names.to_string())
396 .with_fact(FACT_COLUMN_INDEX, i as usize));
397 }
398
399 let cstr = unsafe { CStr::from_ptr(name_ptr) };
402 let name = cstr.to_str().map_err(|_| {
403 error::foreign_invariant_violation(
404 operation,
405 "column name from FFI is not valid UTF-8",
406 "column_name",
407 "invalid_utf8",
408 "ensure the foreign reader returns valid UTF-8 column names",
409 )
410 .with_detail("column_name_count", num_names.to_string())
411 .with_fact(FACT_COLUMN_INDEX, i as usize)
412 })?;
413 names.push(name.to_owned());
414 }
415
416 Ok(names)
417}
418
419#[cfg(test)]
420mod tests {
421 use std::{ffi::CString, fs, os::raw::c_char, ptr};
422
423 use crate::core::error::{ErrorCode, ErrorKind, ErrorStatus, FactKeyName, FactValue};
424
425 #[test]
426 fn kvikio_bounce_buffer_drain_busy_error_is_flat_and_retryable() {
427 let error = super::kvikio_drain_result(0, 2).unwrap_err();
428 assert_eq!(error.code(), ErrorCode::IoBusy);
429 assert_eq!(error.kind(), ErrorKind::ResourceExhausted);
430 assert_eq!(error.status(), ErrorStatus::Temporary);
431 assert_eq!(error.detail_value("active_io"), Some("2"));
432 assert_eq!(error.detail_value("reason"), Some("active_io"));
433 }
434
435 #[test]
436 fn kvikio_bounce_buffer_drain_idle_result_is_idempotent() {
437 assert_eq!(super::kvikio_drain_result(16 * 1024 * 1024, 0).unwrap(), 16 * 1024 * 1024);
438 assert_eq!(super::kvikio_drain_result(0, 0).unwrap(), 0);
439 }
440
441 #[test]
442 fn test_collect_column_names_rejects_negative_count() {
443 let err = unsafe { super::collect_column_names(ptr::null_mut(), -1, "test") }.unwrap_err();
444 assert_eq!(err.kind(), ErrorKind::InvariantViolation);
445 assert_eq!(err.code(), ErrorCode::ForeignInvariantViolation);
446 assert_eq!(err.operation(), "test");
447 assert!(err.details().iter().any(|(k, v)| *k == "column_name_count" && v == "-1"));
448 assert!(!err.details().iter().any(|(k, _)| *k == "num_names"));
449 assert!(err.details().iter().any(|(k, v)| *k == "resource" && v == "column_name_count"));
450 assert!(err.details().iter().any(|(k, v)| *k == "reason" && v == "negative_name_count"));
451 assert!(
452 err.details()
453 .iter()
454 .any(|(k, v)| *k == "remedy"
455 && v == "ensure the foreign reader reports a non-negative column name count")
456 );
457 }
458
459 #[test]
460 fn test_collect_column_names_rejects_null_names_ptr_with_positive_count() {
461 let err = unsafe { super::collect_column_names(ptr::null_mut(), 1, "test") }.unwrap_err();
462 assert_eq!(err.kind(), ErrorKind::InvariantViolation);
463 assert_eq!(err.code(), ErrorCode::ForeignInvariantViolation);
464 assert_eq!(err.operation(), "test");
465 assert!(err.details().iter().any(|(k, v)| *k == "column_name_count" && v == "1"));
466 assert!(err.details().iter().any(|(k, v)| *k == "resource" && v == "column_names"));
467 assert!(err.details().iter().any(|(k, v)| *k == "reason" && v == "null_name_array"));
468 assert!(err.details().iter().any(|(k, v)| *k == "remedy"
469 && v == "ensure the foreign reader returns a name array whenever column_name_count is positive"));
470 assert!(!err.details().iter().any(|(k, _)| *k == "names_ptr_null"));
471 }
472
473 #[test]
474 fn test_collect_column_names_rejects_null_entry_pointer() {
475 let mut ptrs: Vec<*mut c_char> = vec![ptr::null_mut()];
476 let err = unsafe { super::collect_column_names(ptrs.as_mut_ptr(), 1, "test") }.unwrap_err();
477 assert_eq!(err.kind(), ErrorKind::InvariantViolation);
478 assert_eq!(err.code(), ErrorCode::ForeignInvariantViolation);
479 assert_eq!(err.operation(), "test");
480 assert_eq!(err.fact_value(FactKeyName::ColumnIndex), Some(&FactValue::Usize(0)));
481 assert!(!err.details().iter().any(|(k, _)| *k == "column_index"));
482 assert!(err.details().iter().any(|(k, v)| *k == "resource" && v == "column_name"));
483 assert!(err.details().iter().any(|(k, v)| *k == "reason" && v == "null_name_entry"));
484 assert!(err.details().iter().any(|(k, v)| *k == "remedy"
485 && v == "ensure the foreign reader returns one non-null name entry per reported column"));
486 assert!(!err.details().iter().any(|(k, _)| *k == "name_index"));
487 }
488
489 #[test]
490 fn test_collect_column_names_success() {
491 let mut raw = [CString::new("alpha").unwrap(), CString::new("beta").unwrap()];
492 let mut ptrs: Vec<*mut c_char> = raw.iter_mut().map(|s| s.as_ptr() as *mut c_char).collect();
493 let names = unsafe { super::collect_column_names(ptrs.as_mut_ptr(), ptrs.len() as i32, "test") }.unwrap();
494 assert_eq!(names, vec!["alpha", "beta"]);
495 }
496
497 #[test]
498 fn test_validate_input_path_reports_io_failure() {
499 let path = std::env::temp_dir().join("cudf_rs_missing_input_path_test_12345.csv");
500 let err = super::validate_input_path(&path, "test").unwrap_err();
501
502 assert_eq!(err.code(), ErrorCode::IoFailure);
507 assert_eq!(err.kind(), ErrorKind::DependencyFailure);
508 assert_eq!(err.operation(), "test");
509 assert!(err.details().iter().any(|(k, v)| *k == "path" && v == &path.display().to_string()));
510 }
511
512 #[test]
513 fn test_validate_input_path_rejects_directory() {
514 let path = std::env::temp_dir().join(format!("cudf_rs_directory_input_path_test_{}", std::process::id()));
515 fs::create_dir_all(&path).unwrap();
516
517 let result = super::validate_input_path(&path, "test");
518 let _ = fs::remove_dir(&path);
519 let err = result.unwrap_err();
520 let path_display = path.display().to_string();
521
522 assert_eq!(err.code(), ErrorCode::PathIsDirectory);
523 assert_eq!(err.code().to_string(), "PathIsDirectory");
524 assert_eq!(err.kind(), ErrorKind::InvalidInput);
525 assert_eq!(err.status(), ErrorStatus::Permanent);
526 assert_eq!(err.operation(), "test");
527 assert_eq!(err.detail_value("path"), Some(path_display.as_str()));
528 assert_eq!(err.detail_value("resource"), Some("input_path"));
529 assert_eq!(err.detail_value("reason"), Some("path_is_directory"));
530 assert_eq!(err.detail_value("remedy"), Some("provide a regular file path instead of a directory"));
531 }
532}