1use axum::{
10 body::Body,
11 extract::{Path, Query, State},
12 http::{header, HeaderMap, StatusCode},
13 response::{IntoResponse, Response},
14 Json,
15};
16use ipfrs_core::Cid;
17use ipfrs_storage::BlockStoreTrait;
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20use std::sync::Arc;
21
22use crate::gateway::GatewayState;
23use crate::middleware::{
24 add_caching_headers, check_etag_match, not_modified_response, CacheConfig,
25};
26use crate::mmap::{MmapCache, MmapError};
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TensorMetadata {
35 pub shape: Vec<usize>,
37 pub dtype: String,
39 pub num_elements: usize,
41 pub size_bytes: usize,
43 pub layout: TensorLayout,
45}
46
47#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
49#[serde(rename_all = "lowercase")]
50pub enum TensorLayout {
51 RowMajor,
53 ColumnMajor,
55}
56
57impl TensorMetadata {
58 pub fn from_safetensors_data(data: &[u8]) -> Result<Self, String> {
60 if data.len() < 8 {
62 return Err("Data too short for safetensors format".to_string());
63 }
64
65 let header_len = u64::from_le_bytes(
66 data[0..8]
67 .try_into()
68 .expect("data[0..8] is exactly 8 bytes after bounds check"),
69 ) as usize;
70 if data.len() < 8 + header_len {
71 return Err("Incomplete safetensors header".to_string());
72 }
73
74 let header_bytes = &data[8..8 + header_len];
76 let header: serde_json::Value = serde_json::from_slice(header_bytes)
77 .map_err(|e| format!("Failed to parse safetensors header: {}", e))?;
78
79 if let Some(tensors) = header.as_object() {
81 if let Some((_name, tensor_info)) =
82 tensors.iter().find(|(k, _)| k.as_str() != "__metadata__")
83 {
84 if let Some(shape) = tensor_info.get("shape").and_then(|s| s.as_array()) {
85 let shape: Vec<usize> = shape
86 .iter()
87 .filter_map(|v| v.as_u64().map(|n| n as usize))
88 .collect();
89
90 let dtype = tensor_info
91 .get("dtype")
92 .and_then(|d| d.as_str())
93 .unwrap_or("f32")
94 .to_string();
95
96 let num_elements = shape.iter().product();
97 let element_size = Self::dtype_size(&dtype);
98 let size_bytes = num_elements * element_size;
99
100 return Ok(TensorMetadata {
101 shape,
102 dtype,
103 num_elements,
104 size_bytes,
105 layout: TensorLayout::RowMajor, });
107 }
108 }
109 }
110
111 Err("No tensor found in safetensors data".to_string())
112 }
113
114 fn dtype_size(dtype: &str) -> usize {
116 match dtype {
117 "f16" | "bf16" => 2,
118 "f32" | "i32" | "u32" => 4,
119 "f64" | "i64" | "u64" => 8,
120 "i8" | "u8" => 1,
121 "i16" | "u16" => 2,
122 _ => 4, }
124 }
125
126 pub fn from_raw(shape: Vec<usize>, dtype: String) -> Self {
128 let num_elements = shape.iter().product();
129 let element_size = Self::dtype_size(&dtype);
130 let size_bytes = num_elements * element_size;
131
132 TensorMetadata {
133 shape,
134 dtype,
135 num_elements,
136 size_bytes,
137 layout: TensorLayout::RowMajor,
138 }
139 }
140}
141
142#[derive(Debug, Deserialize)]
148pub struct TensorQuery {
149 pub metadata_only: Option<bool>,
151 pub slice: Option<String>,
153 pub format: Option<String>,
155}
156
157#[derive(Debug)]
159pub struct TensorSlice {
160 pub ranges: Vec<(usize, Option<usize>)>,
162}
163
164impl TensorSlice {
165 pub fn extract_data(&self, data: &[u8], metadata: &TensorMetadata) -> Result<Vec<u8>, String> {
170 if self.ranges.len() != metadata.shape.len() {
171 return Err(format!(
172 "Slice dimensions ({}) don't match tensor dimensions ({})",
173 self.ranges.len(),
174 metadata.shape.len()
175 ));
176 }
177
178 let element_size = TensorMetadata::dtype_size(&metadata.dtype);
179
180 match metadata.shape.len() {
183 1 => self.extract_1d(data, &metadata.shape, element_size),
184 2 => self.extract_2d(data, &metadata.shape, element_size),
185 _ => self.extract_nd(data, &metadata.shape, element_size),
186 }
187 }
188
189 fn extract_nd(
196 &self,
197 data: &[u8],
198 shape: &[usize],
199 element_size: usize,
200 ) -> Result<Vec<u8>, String> {
201 let ndim = shape.len();
202
203 let mut starts = Vec::with_capacity(ndim);
205 let mut ends = Vec::with_capacity(ndim);
206 for (dim, &(start, end_opt)) in self.ranges.iter().enumerate() {
207 let end = end_opt.unwrap_or(shape[dim]);
208 if start >= shape[dim] {
209 return Err(format!(
210 "Slice start {} out of bounds for dimension {} (size {})",
211 start, dim, shape[dim]
212 ));
213 }
214 if end > shape[dim] {
215 return Err(format!(
216 "Slice end {} out of bounds for dimension {} (size {})",
217 end, dim, shape[dim]
218 ));
219 }
220 if start >= end {
221 return Err(format!(
222 "Slice start {} >= end {} for dimension {}",
223 start, end, dim
224 ));
225 }
226 starts.push(start);
227 ends.push(end);
228 }
229
230 let mut strides = vec![1usize; ndim];
232 for d in (0..ndim - 1).rev() {
233 strides[d] = strides[d + 1] * shape[d + 1];
234 }
235
236 let out_elements: usize = starts
238 .iter()
239 .zip(ends.iter())
240 .map(|(&s, &e)| e - s)
241 .product();
242 let mut result = vec![0u8; out_elements * element_size];
243
244 let mut indices = starts.clone();
247 for out_elem in 0..out_elements {
248 let src_elem: usize = indices
250 .iter()
251 .zip(strides.iter())
252 .map(|(&i, &s)| i * s)
253 .sum();
254 let src_byte = src_elem * element_size;
255 if src_byte + element_size > data.len() {
256 return Err(format!(
257 "Source byte range {}..{} exceeds data length {}",
258 src_byte,
259 src_byte + element_size,
260 data.len()
261 ));
262 }
263 let dst_byte = out_elem * element_size;
264 result[dst_byte..dst_byte + element_size]
265 .copy_from_slice(&data[src_byte..src_byte + element_size]);
266
267 let mut carry = true;
269 for d in (0..ndim).rev() {
270 if carry {
271 indices[d] += 1;
272 if indices[d] >= ends[d] {
273 indices[d] = starts[d];
274 } else {
276 carry = false;
277 }
278 }
279 }
280 }
281
282 Ok(result)
283 }
284
285 fn extract_1d(
287 &self,
288 data: &[u8],
289 shape: &[usize],
290 element_size: usize,
291 ) -> Result<Vec<u8>, String> {
292 let (start, end) = (self.ranges[0].0, self.ranges[0].1.unwrap_or(shape[0]));
293
294 if start >= shape[0] || end > shape[0] || start >= end {
295 return Err(format!(
296 "Invalid 1D slice range [{}:{}] for shape [{}]",
297 start, end, shape[0]
298 ));
299 }
300
301 let byte_start = start * element_size;
302 let byte_end = end * element_size;
303
304 if byte_end > data.len() {
305 return Err(format!(
306 "Slice range {}..{} exceeds data length {}",
307 byte_start,
308 byte_end,
309 data.len()
310 ));
311 }
312
313 Ok(data[byte_start..byte_end].to_vec())
314 }
315
316 fn extract_2d(
318 &self,
319 data: &[u8],
320 shape: &[usize],
321 element_size: usize,
322 ) -> Result<Vec<u8>, String> {
323 let rows = shape[0];
324 let cols = shape[1];
325
326 let (row_start, row_end) = (self.ranges[0].0, self.ranges[0].1.unwrap_or(rows));
327 let (col_start, col_end) = (self.ranges[1].0, self.ranges[1].1.unwrap_or(cols));
328
329 if row_start >= rows || row_end > rows || row_start >= row_end {
330 return Err(format!(
331 "Invalid row range [{}:{}] for shape [{}, {}]",
332 row_start, row_end, rows, cols
333 ));
334 }
335
336 if col_start >= cols || col_end > cols || col_start >= col_end {
337 return Err(format!(
338 "Invalid column range [{}:{}] for shape [{}, {}]",
339 col_start, col_end, rows, cols
340 ));
341 }
342
343 let mut result = Vec::new();
344 let row_size = cols * element_size;
345
346 for row in row_start..row_end {
347 let row_offset = row * row_size;
348 let slice_start = row_offset + col_start * element_size;
349 let slice_end = row_offset + col_end * element_size;
350
351 if slice_end > data.len() {
352 return Err(format!(
353 "Row {} slice range {}..{} exceeds data length {}",
354 row,
355 slice_start,
356 slice_end,
357 data.len()
358 ));
359 }
360
361 result.extend_from_slice(&data[slice_start..slice_end]);
362 }
363
364 Ok(result)
365 }
366
367 pub fn parse(slice_str: &str) -> Result<Self, String> {
369 let ranges: Result<Vec<_>, String> = slice_str
370 .split(',')
371 .map(|part| {
372 let parts: Vec<&str> = part.split(':').collect();
373 match parts.len() {
374 1 => {
375 let idx = parts[0]
376 .parse::<usize>()
377 .map_err(|e| format!("Invalid slice index: {}", e))?;
378 Ok((idx, Some(idx + 1)))
379 }
380 2 => {
381 let start = parts[0]
382 .parse::<usize>()
383 .map_err(|e| format!("Invalid slice start: {}", e))?;
384 let end = if parts[1].is_empty() {
385 None
386 } else {
387 Some(
388 parts[1]
389 .parse::<usize>()
390 .map_err(|e| format!("Invalid slice end: {}", e))?,
391 )
392 };
393 Ok((start, end))
394 }
395 _ => Err(format!("Invalid slice format: {}", part)),
396 }
397 })
398 .collect();
399
400 Ok(TensorSlice { ranges: ranges? })
401 }
402
403 pub fn calculate_size(&self, metadata: &TensorMetadata) -> Result<usize, String> {
405 if self.ranges.len() != metadata.shape.len() {
406 return Err(format!(
407 "Slice dimensions ({}) don't match tensor dimensions ({})",
408 self.ranges.len(),
409 metadata.shape.len()
410 ));
411 }
412
413 let mut slice_elements = 1;
414 for (i, (start, end)) in self.ranges.iter().enumerate() {
415 let dim_size = metadata.shape[i];
416 let actual_end = end.unwrap_or(dim_size);
417
418 if *start >= dim_size || actual_end > dim_size || *start >= actual_end {
419 return Err(format!(
420 "Invalid slice range [{}:{}] for dimension {} of size {}",
421 start, actual_end, i, dim_size
422 ));
423 }
424
425 slice_elements *= actual_end - start;
426 }
427
428 let element_size = TensorMetadata::dtype_size(&metadata.dtype);
429 Ok(slice_elements * element_size)
430 }
431}
432
433#[derive(Debug, Serialize)]
439pub struct TensorInfoResponse {
440 pub cid: String,
441 pub metadata: TensorMetadata,
442}
443
444pub async fn get_tensor(
455 State(state): State<GatewayState>,
456 Path(cid_str): Path<String>,
457 Query(query): Query<TensorQuery>,
458 headers: HeaderMap,
459) -> Result<Response, TensorError> {
460 let cid: Cid = cid_str
461 .parse()
462 .map_err(|_| TensorError::InvalidCid(cid_str.clone()))?;
463
464 let cache_config = CacheConfig::default();
466 if check_etag_match(&headers, &cid_str) {
467 return Ok(not_modified_response(&cid_str, &cache_config));
468 }
469
470 let block = state
472 .store
473 .get(&cid)
474 .await
475 .map_err(|e| TensorError::Storage(e.to_string()))?
476 .ok_or_else(|| TensorError::NotFound(cid_str.clone()))?;
477
478 let data = block.data();
479
480 let metadata = TensorMetadata::from_safetensors_data(data).ok();
482
483 if query.metadata_only.unwrap_or(false) {
485 if let Some(metadata) = metadata {
486 return Ok(Json(TensorInfoResponse {
487 cid: cid_str,
488 metadata,
489 })
490 .into_response());
491 } else {
492 return Err(TensorError::InvalidFormat(
493 "Cannot extract metadata from tensor".to_string(),
494 ));
495 }
496 }
497
498 let (response_data, is_partial, metadata_for_response) = if let Some(slice_str) = query.slice {
500 let meta = metadata.ok_or_else(|| {
501 TensorError::InvalidFormat("Metadata required for slicing".to_string())
502 })?;
503
504 let slice = TensorSlice::parse(&slice_str)?;
505
506 let sliced_data = slice.extract_data(data, &meta)?;
508
509 (sliced_data, true, Some(meta))
510 } else {
511 (data.to_vec(), false, metadata)
513 };
514
515 let mut response_builder = Response::builder();
517
518 if is_partial {
519 response_builder = response_builder.status(StatusCode::PARTIAL_CONTENT);
520 } else {
521 response_builder = response_builder.status(StatusCode::OK);
522 }
523
524 let content_type = match query.format.as_deref() {
526 Some("safetensors") | None if metadata_for_response.is_some() => {
527 "application/vnd.safetensors"
528 }
529 _ => "application/octet-stream",
530 };
531
532 let mut response = response_builder
533 .header(header::CONTENT_TYPE, content_type)
534 .header(header::CONTENT_LENGTH, response_data.len())
535 .header(
536 "X-Tensor-Format",
537 if metadata_for_response.is_some() {
538 "safetensors"
539 } else {
540 "raw"
541 },
542 )
543 .body(Body::from(response_data))
544 .expect("building HTTP response with valid headers and body is infallible");
545
546 add_caching_headers(response.headers_mut(), &cid_str, &cache_config);
548
549 if let Some(ref meta) = metadata_for_response {
551 if let Ok(shape_json) = serde_json::to_string(&meta.shape) {
552 if let Ok(header_value) = header::HeaderValue::from_str(&shape_json) {
553 response
554 .headers_mut()
555 .insert("X-Tensor-Shape", header_value);
556 }
557 }
558 if let Ok(header_value) = header::HeaderValue::from_str(&meta.dtype) {
559 response
560 .headers_mut()
561 .insert("X-Tensor-Dtype", header_value);
562 }
563 }
564
565 Ok(response)
566}
567
568pub async fn get_tensor_info(
574 State(state): State<GatewayState>,
575 Path(cid_str): Path<String>,
576) -> Result<Json<TensorInfoResponse>, TensorError> {
577 let cid: Cid = cid_str
578 .parse()
579 .map_err(|_| TensorError::InvalidCid(cid_str.clone()))?;
580
581 let block = state
583 .store
584 .get(&cid)
585 .await
586 .map_err(|e| TensorError::Storage(e.to_string()))?
587 .ok_or_else(|| TensorError::NotFound(cid_str.clone()))?;
588
589 let data = block.data();
590
591 let metadata = TensorMetadata::from_safetensors_data(data).map_err(|e| {
593 TensorError::InvalidFormat(format!("Failed to parse tensor metadata: {}", e))
594 })?;
595
596 Ok(Json(TensorInfoResponse {
597 cid: cid_str,
598 metadata,
599 }))
600}
601
602pub async fn get_tensor_arrow(
609 State(state): State<GatewayState>,
610 Path(cid_str): Path<String>,
611 Query(query): Query<TensorQuery>,
612) -> Result<Response, TensorError> {
613 let cid: Cid = cid_str
614 .parse()
615 .map_err(|_| TensorError::InvalidCid(cid_str.clone()))?;
616
617 let block = state
619 .store
620 .get(&cid)
621 .await
622 .map_err(|e| TensorError::Storage(e.to_string()))?
623 .ok_or_else(|| TensorError::NotFound(cid_str.clone()))?;
624
625 let data = block.data();
626
627 let metadata = TensorMetadata::from_safetensors_data(data)
629 .map_err(|e| TensorError::InvalidFormat(format!("Cannot parse tensor metadata: {}", e)))?;
630
631 let response_data = if let Some(slice_str) = query.slice {
633 let slice = TensorSlice::parse(&slice_str)?;
634 slice.extract_data(data, &metadata)?
635 } else {
636 let header_len = u64::from_le_bytes(
638 data[0..8]
639 .try_into()
640 .expect("data[0..8] is exactly 8 bytes"),
641 ) as usize;
642 data[8 + header_len..].to_vec()
643 };
644
645 let batch = crate::arrow::tensor_to_record_batch(&metadata, &response_data)
647 .map_err(|e| TensorError::Storage(format!("Failed to create Arrow batch: {}", e)))?;
648
649 let ipc_bytes = crate::arrow::record_batch_to_ipc_bytes(&batch)
650 .map_err(|e| TensorError::Storage(format!("Failed to serialize Arrow IPC: {}", e)))?;
651
652 Response::builder()
654 .status(StatusCode::OK)
655 .header(header::CONTENT_TYPE, "application/vnd.apache.arrow.stream")
656 .header("X-Tensor-Shape", format!("{:?}", metadata.shape))
657 .header("X-Tensor-Dtype", &metadata.dtype)
658 .header("X-Tensor-Elements", metadata.num_elements.to_string())
659 .body(Body::from(ipc_bytes))
660 .map_err(|e| TensorError::Storage(format!("Failed to build response: {}", e)))
661}
662
663pub async fn get_tensor_mmap(
687 Path(cid_str): Path<String>,
688 Query(query): Query<TensorQuery>,
689 headers: HeaderMap,
690 mmap_cache: Arc<MmapCache>,
691 tensor_storage_path: PathBuf,
692) -> Result<Response, TensorError> {
693 let _cid: Cid = cid_str
694 .parse()
695 .map_err(|_| TensorError::InvalidCid(cid_str.clone()))?;
696
697 let cache_config = CacheConfig::default();
699 if check_etag_match(&headers, &cid_str) {
700 return Ok(not_modified_response(&cid_str, &cache_config));
701 }
702
703 let file_path = tensor_storage_path.join(format!("{}.tensor", cid_str));
706
707 let mmap_file = mmap_cache.get_or_create(&file_path).map_err(|e| match e {
709 MmapError::FileNotFound(_) => TensorError::NotFound(cid_str.clone()),
710 _ => TensorError::Storage(e.to_string()),
711 })?;
712
713 let data = mmap_file.bytes();
715
716 let metadata = TensorMetadata::from_safetensors_data(&data).ok();
718
719 if query.metadata_only.unwrap_or(false) {
721 if let Some(metadata) = metadata {
722 return Ok(Json(TensorInfoResponse {
723 cid: cid_str,
724 metadata,
725 })
726 .into_response());
727 } else {
728 return Err(TensorError::InvalidFormat(
729 "Cannot extract metadata from tensor".to_string(),
730 ));
731 }
732 }
733
734 let (response_data, is_partial, metadata_for_response) = if let Some(slice_str) = query.slice {
736 let meta = metadata.ok_or_else(|| {
737 TensorError::InvalidFormat("Metadata required for slicing".to_string())
738 })?;
739
740 let slice = TensorSlice::parse(&slice_str)?;
741
742 let sliced_data = slice.extract_data(&data, &meta)?;
745
746 (sliced_data, true, Some(meta))
747 } else {
748 (data.to_vec(), false, metadata)
750 };
751
752 let mut response_builder = Response::builder();
754
755 if is_partial {
756 response_builder = response_builder.status(StatusCode::PARTIAL_CONTENT);
757 } else {
758 response_builder = response_builder.status(StatusCode::OK);
759 }
760
761 let content_type = match query.format.as_deref() {
763 Some("safetensors") | None if metadata_for_response.is_some() => {
764 "application/vnd.safetensors"
765 }
766 _ => "application/octet-stream",
767 };
768
769 let mut response = response_builder
770 .header(header::CONTENT_TYPE, content_type)
771 .header(header::CONTENT_LENGTH, response_data.len())
772 .header("X-Served-By", "mmap")
773 .header(
774 "X-Tensor-Format",
775 if metadata_for_response.is_some() {
776 "safetensors"
777 } else {
778 "raw"
779 },
780 )
781 .body(Body::from(response_data))
782 .expect("building HTTP response with valid headers and body is infallible");
783
784 add_caching_headers(response.headers_mut(), &cid_str, &cache_config);
786
787 if let Some(ref meta) = metadata_for_response {
789 if let Ok(shape_json) = serde_json::to_string(&meta.shape) {
790 if let Ok(header_value) = header::HeaderValue::from_str(&shape_json) {
791 response
792 .headers_mut()
793 .insert("X-Tensor-Shape", header_value);
794 }
795 }
796 if let Ok(header_value) = header::HeaderValue::from_str(&meta.dtype) {
797 response
798 .headers_mut()
799 .insert("X-Tensor-Dtype", header_value);
800 }
801 }
802
803 Ok(response)
804}
805
806#[allow(dead_code)]
811pub async fn get_tensor_mmap_range(
812 cid_str: String,
813 range: std::ops::Range<usize>,
814 mmap_cache: Arc<MmapCache>,
815 tensor_storage_path: PathBuf,
816) -> Result<Response, TensorError> {
817 let _cid: Cid = cid_str
818 .parse()
819 .map_err(|_| TensorError::InvalidCid(cid_str.clone()))?;
820
821 let file_path = tensor_storage_path.join(format!("{}.tensor", cid_str));
823
824 let mmap_file = mmap_cache.get_or_create(&file_path).map_err(|e| match e {
826 MmapError::FileNotFound(_) => TensorError::NotFound(cid_str.clone()),
827 _ => TensorError::Storage(e.to_string()),
828 })?;
829
830 let range_data = mmap_file
832 .range(range.clone())
833 .map_err(|e| TensorError::Storage(e.to_string()))?;
834
835 let response = Response::builder()
837 .status(StatusCode::PARTIAL_CONTENT)
838 .header(header::CONTENT_TYPE, "application/octet-stream")
839 .header(header::CONTENT_LENGTH, range_data.len())
840 .header(
841 header::CONTENT_RANGE,
842 format!(
843 "bytes {}-{}/{}",
844 range.start,
845 range.end - 1,
846 mmap_file.size()
847 ),
848 )
849 .header("X-Served-By", "mmap")
850 .body(Body::from(range_data))
851 .expect("building PARTIAL_CONTENT response with valid headers and body is infallible");
852
853 Ok(response)
854}
855
856#[derive(Debug)]
862pub enum TensorError {
863 InvalidCid(String),
864 NotFound(String),
865 InvalidFormat(String),
866 Storage(String),
867 NotImplemented(String),
868}
869
870impl IntoResponse for TensorError {
871 fn into_response(self) -> Response {
872 let (status, message) = match self {
873 TensorError::InvalidCid(cid) => {
874 (StatusCode::BAD_REQUEST, format!("Invalid CID: {}", cid))
875 }
876 TensorError::NotFound(cid) => {
877 (StatusCode::NOT_FOUND, format!("Tensor not found: {}", cid))
878 }
879 TensorError::InvalidFormat(msg) => (
880 StatusCode::BAD_REQUEST,
881 format!("Invalid tensor format: {}", msg),
882 ),
883 TensorError::Storage(msg) => (
884 StatusCode::INTERNAL_SERVER_ERROR,
885 format!("Storage error: {}", msg),
886 ),
887 TensorError::NotImplemented(msg) => (
888 StatusCode::NOT_IMPLEMENTED,
889 format!("Not implemented: {}", msg),
890 ),
891 };
892
893 (status, message).into_response()
894 }
895}
896
897impl From<String> for TensorError {
898 fn from(s: String) -> Self {
899 TensorError::InvalidFormat(s)
900 }
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906
907 #[test]
908 fn test_tensor_metadata_dtype_size() {
909 assert_eq!(TensorMetadata::dtype_size("f32"), 4);
910 assert_eq!(TensorMetadata::dtype_size("f64"), 8);
911 assert_eq!(TensorMetadata::dtype_size("i32"), 4);
912 assert_eq!(TensorMetadata::dtype_size("u8"), 1);
913 assert_eq!(TensorMetadata::dtype_size("f16"), 2);
914 }
915
916 #[test]
917 fn test_tensor_metadata_from_raw() {
918 let meta = TensorMetadata::from_raw(vec![10, 20, 30], "f32".to_string());
919 assert_eq!(meta.shape, vec![10, 20, 30]);
920 assert_eq!(meta.dtype, "f32");
921 assert_eq!(meta.num_elements, 6000);
922 assert_eq!(meta.size_bytes, 24000);
923 }
924
925 #[test]
926 fn test_tensor_slice_parse_single() {
927 let slice = TensorSlice::parse("5").expect("test: parse single index should succeed");
928 assert_eq!(slice.ranges, vec![(5, Some(6))]);
929 }
930
931 #[test]
932 fn test_tensor_slice_parse_range() {
933 let slice = TensorSlice::parse("10:20").expect("test: parse range slice should succeed");
934 assert_eq!(slice.ranges, vec![(10, Some(20))]);
935 }
936
937 #[test]
938 fn test_tensor_slice_parse_open_end() {
939 let slice = TensorSlice::parse("10:").expect("test: parse open-end slice should succeed");
940 assert_eq!(slice.ranges, vec![(10, None)]);
941 }
942
943 #[test]
944 fn test_tensor_slice_parse_multi_dim() {
945 let slice = TensorSlice::parse("0:10,5:15,2:8")
946 .expect("test: parse multi-dim slice should succeed");
947 assert_eq!(
948 slice.ranges,
949 vec![(0, Some(10)), (5, Some(15)), (2, Some(8))]
950 );
951 }
952
953 #[test]
954 fn test_tensor_slice_calculate_size() {
955 let meta = TensorMetadata::from_raw(vec![100, 100], "f32".to_string());
956 let slice = TensorSlice::parse("0:10,0:10").expect("test: parse 2D slice should succeed");
957
958 let size = slice
959 .calculate_size(&meta)
960 .expect("test: size calculation should succeed");
961 assert_eq!(size, 10 * 10 * 4); }
963
964 #[test]
965 fn test_tensor_slice_invalid_dimensions() {
966 let meta = TensorMetadata::from_raw(vec![100, 100], "f32".to_string());
967 let slice = TensorSlice::parse("0:10").expect("test: parse 1D slice should succeed"); let result = slice.calculate_size(&meta);
970 assert!(result.is_err());
971 }
972
973 #[test]
974 fn test_tensor_slice_out_of_bounds() {
975 let meta = TensorMetadata::from_raw(vec![100, 100], "f32".to_string());
976 let slice = TensorSlice::parse("0:200,0:10")
977 .expect("test: parse out-of-bounds slice should succeed");
978
979 let result = slice.calculate_size(&meta);
980 assert!(result.is_err());
981 }
982
983 #[test]
984 fn test_tensor_layout_serialization() {
985 let layout = TensorLayout::RowMajor;
986 let json =
987 serde_json::to_string(&layout).expect("test: RowMajor serialization should succeed");
988 assert_eq!(json, r#""rowmajor""#);
989
990 let layout = TensorLayout::ColumnMajor;
991 let json =
992 serde_json::to_string(&layout).expect("test: ColumnMajor serialization should succeed");
993 assert_eq!(json, r#""columnmajor""#);
994 }
995
996 #[test]
997 fn test_tensor_slice_extract_1d() {
998 let data: Vec<u8> = (0..10).flat_map(|i| (i as f32).to_le_bytes()).collect();
1000
1001 let meta = TensorMetadata::from_raw(vec![10], "f32".to_string());
1002 let slice = TensorSlice::parse("2:5").expect("test: parse 1D range should succeed");
1003
1004 let result = slice
1005 .extract_data(&data, &meta)
1006 .expect("test: 1D slice extraction should succeed");
1007
1008 assert_eq!(result.len(), 12);
1010
1011 let values: Vec<f32> = result
1013 .chunks_exact(4)
1014 .map(|chunk| {
1015 f32::from_le_bytes(
1016 chunk
1017 .try_into()
1018 .expect("test: chunk to [u8;4] conversion should succeed"),
1019 )
1020 })
1021 .collect();
1022
1023 assert_eq!(values, vec![2.0, 3.0, 4.0]);
1024 }
1025
1026 #[test]
1027 fn test_tensor_slice_extract_2d() {
1028 let data: Vec<u8> = (0..12).flat_map(|i| (i as f32).to_le_bytes()).collect();
1034
1035 let meta = TensorMetadata::from_raw(vec![4, 3], "f32".to_string());
1036 let slice =
1037 TensorSlice::parse("1:3,0:2").expect("test: parse 2D row/col slice should succeed"); let result = slice
1040 .extract_data(&data, &meta)
1041 .expect("test: 2D slice extraction should succeed");
1042
1043 assert_eq!(result.len(), 16);
1048
1049 let values: Vec<f32> = result
1050 .chunks_exact(4)
1051 .map(|chunk| {
1052 f32::from_le_bytes(
1053 chunk
1054 .try_into()
1055 .expect("test: chunk to [u8;4] conversion should succeed"),
1056 )
1057 })
1058 .collect();
1059
1060 assert_eq!(values, vec![3.0, 4.0, 6.0, 7.0]);
1061 }
1062
1063 #[test]
1064 fn test_tensor_slice_extract_2d_single_row() {
1065 let data: Vec<u8> = (0..12).flat_map(|i| (i as f32).to_le_bytes()).collect();
1066
1067 let meta = TensorMetadata::from_raw(vec![4, 3], "f32".to_string());
1068 let slice =
1069 TensorSlice::parse("2:3,0:3").expect("test: parse single-row slice should succeed"); let result = slice
1072 .extract_data(&data, &meta)
1073 .expect("test: single-row extraction should succeed");
1074
1075 assert_eq!(result.len(), 12);
1077
1078 let values: Vec<f32> = result
1079 .chunks_exact(4)
1080 .map(|chunk| {
1081 f32::from_le_bytes(
1082 chunk
1083 .try_into()
1084 .expect("test: chunk to [u8;4] conversion should succeed"),
1085 )
1086 })
1087 .collect();
1088
1089 assert_eq!(values, vec![6.0, 7.0, 8.0]);
1090 }
1091
1092 #[test]
1093 fn test_tensor_slice_extract_invalid_dimension() {
1094 let data = vec![0u8; 40]; let meta = TensorMetadata::from_raw(vec![10], "f32".to_string());
1096 let slice = TensorSlice::parse("2:5,0:2")
1097 .expect("test: parse 2D slice for 1D tensor should succeed"); let result = slice.extract_data(&data, &meta);
1100 assert!(result.is_err());
1101 }
1102
1103 #[test]
1104 fn test_tensor_slice_extract_out_of_bounds() {
1105 let data: Vec<u8> = (0..10).flat_map(|i| (i as f32).to_le_bytes()).collect();
1106
1107 let meta = TensorMetadata::from_raw(vec![10], "f32".to_string());
1108 let slice =
1109 TensorSlice::parse("8:12").expect("test: parse out-of-bounds range should succeed"); let result = slice.extract_data(&data, &meta);
1112 assert!(result.is_err());
1113 }
1114}