1use crate::error::{Error, Result};
8use crate::prelude::*;
9use crate::tensor::{DType, Tensor};
10
11const MAGIC: &[u8; 6] = b"\x93NUMPY";
12const ALIGNMENT: usize = 64;
13
14pub fn from_npy(data: &[u8]) -> Result<Tensor> {
16 if data.len() < 10 || &data[..6] != MAGIC {
17 return Err(Error::format("not a .npy file (bad magic)"));
18 }
19 let major = data[6];
20 let (header_len, header_start) = match major {
21 1 => (u16::from_le_bytes([data[8], data[9]]) as usize, 10),
22 2 | 3 => {
23 if data.len() < 12 {
24 return Err(Error::format("truncated .npy header"));
25 }
26 (u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize, 12)
27 }
28 other => return Err(Error::unsupported(format!(".npy version {other}"))),
29 };
30 let header_end = header_start + header_len;
31 if data.len() < header_end {
32 return Err(Error::format("truncated .npy header"));
33 }
34 let header = core::str::from_utf8(&data[header_start..header_end])?;
35 let (dtype, big_endian, fortran, shape) = parse_header(header)?;
36
37 let expected = shape.iter().product::<usize>() * dtype.size();
38 let body = &data[header_end..];
39 if body.len() < expected {
40 return Err(Error::format(format!(".npy body is {} bytes but shape {shape:?} needs {expected}", body.len())));
41 }
42 if fortran {
43 return Err(Error::unsupported("Fortran-ordered .npy arrays"));
44 }
45
46 let mut body = body[..expected].to_vec();
47 if big_endian != cfg!(target_endian = "big") && dtype.size() > 1 {
48 for chunk in body.chunks_exact_mut(dtype.size()) {
49 chunk.reverse();
50 }
51 }
52 Tensor::new(dtype, shape, body)
53}
54
55pub fn to_npy(tensor: &Tensor) -> Vec<u8> {
57 let mut out = Vec::with_capacity(tensor.data().len() + ALIGNMENT + 16);
58 let (header, body) = npy_parts(tensor);
59 out.extend_from_slice(&header);
60 out.extend_from_slice(body);
61 out
62}
63
64#[cfg(feature = "std")]
66pub fn write_npy(out: &mut impl std::io::Write, tensor: &Tensor) -> Result<()> {
67 let (header, body) = npy_parts(tensor);
68 out.write_all(&header)?;
69 out.write_all(body)?;
70 Ok(())
71}
72
73#[cfg(feature = "std")]
75pub fn read_npy(stream: &mut impl std::io::Read) -> Result<Tensor> {
76 let mut buf = Vec::new();
77 stream.read_to_end(&mut buf)?;
78 from_npy(&buf)
79}
80
81fn npy_parts(tensor: &Tensor) -> (Vec<u8>, &[u8]) {
83 let order = if cfg!(target_endian = "big") { '>' } else { '<' };
84 let shape = match tensor.shape() {
85 [] => "()".to_string(),
86 [one] => format!("({one},)"),
87 many => format!("({})", many.iter().map(|d| d.to_string()).collect::<Vec<_>>().join(", ")),
88 };
89 let mut header =
90 format!("{{'descr': '{order}{}', 'fortran_order': False, 'shape': {shape}, }}", tensor.dtype().short_name());
91
92 let unpadded = MAGIC.len() + 2 + 2 + header.len() + 1;
94 let padding = (ALIGNMENT - unpadded % ALIGNMENT) % ALIGNMENT;
95 header.push_str(&" ".repeat(padding));
96 header.push('\n');
97
98 let mut block = Vec::with_capacity(unpadded + padding);
99 block.extend_from_slice(MAGIC);
100 block.extend_from_slice(&[1, 0]);
101 block.extend_from_slice(&(header.len() as u16).to_le_bytes());
102 block.extend_from_slice(header.as_bytes());
103 (block, tensor.data())
104}
105
106fn parse_header(header: &str) -> Result<(DType, bool, bool, Vec<usize>)> {
108 let descr = extract(header, "descr").ok_or_else(|| Error::format("no descr in .npy header"))?;
109 let mut chars = descr.chars();
110 let (big_endian, name) = match chars.next() {
111 Some('<') | Some('|') | Some('=') => (false, chars.as_str()),
112 Some('>') => (true, chars.as_str()),
113 _ => (false, descr.as_str()),
114 };
115 let dtype = DType::parse(name)?;
116
117 let fortran = header.contains("'fortran_order': True");
118
119 let shape_text = header
120 .split_once("'shape':")
121 .and_then(|(_, rest)| rest.split_once('(').and_then(|(_, r)| r.split_once(')')).map(|(inner, _)| inner))
122 .ok_or_else(|| Error::format("no shape in .npy header"))?;
123 let shape = shape_text
124 .split(',')
125 .map(str::trim)
126 .filter(|s| !s.is_empty())
127 .map(|s| s.parse::<usize>().map_err(|_| Error::format(format!("bad shape entry {s:?}"))))
128 .collect::<Result<Vec<_>>>()?;
129
130 Ok((dtype, big_endian, fortran, shape))
131}
132
133fn extract(header: &str, key: &str) -> Option<String> {
135 let needle = format!("'{key}':");
136 let rest = header.split_once(&needle)?.1;
137 let rest = rest.trim_start();
138 let quote = rest.chars().next()?;
139 if quote != '\'' && quote != '"' {
140 return None;
141 }
142 let body = &rest[quote.len_utf8()..];
143 let end = body.find(quote)?;
144 Some(body[..end].to_string())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn round_trips_a_matrix() {
153 let t = Tensor::from_f32_shaped(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3]);
154 let bytes = to_npy(&t);
155 assert_eq!(&bytes[..6], MAGIC);
156 let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
158 assert_eq!((10 + header_len) % ALIGNMENT, 0);
159
160 let back = from_npy(&bytes).unwrap();
161 assert_eq!(back, t);
162 }
163
164 #[test]
165 fn round_trips_a_vector_and_a_scalar() {
166 for shape in [vec![4], vec![1], vec![2, 2]] {
167 let n: usize = shape.iter().product();
168 let values: Vec<f32> = (0..n).map(|i| i as f32).collect();
169 let t = Tensor::from_f32_shaped(&values, shape);
170 assert_eq!(from_npy(&to_npy(&t)).unwrap(), t);
171 }
172 }
173
174 #[test]
175 fn rejects_junk() {
176 assert!(from_npy(b"not an npy file at all").is_err());
177 assert!(from_npy(b"").is_err());
178 }
179
180 #[test]
181 fn parses_a_real_numpy_header() {
182 let header = "{'descr': '<i8', 'fortran_order': False, 'shape': (3, 4), }";
183 let (dtype, big, fortran, shape) = parse_header(header).unwrap();
184 assert_eq!(dtype, DType::I64);
185 assert!(!big);
186 assert!(!fortran);
187 assert_eq!(shape, vec![3, 4]);
188 }
189
190 #[test]
191 fn rejects_fortran_order() {
192 let bytes = to_npy(&Tensor::from_f32(&[1.0, 2.0]));
193 let header_len = u16::from_le_bytes([bytes[8], bytes[9]]) as usize;
194 let header = core::str::from_utf8(&bytes[10..10 + header_len]).unwrap().replace("False", "True ");
195
196 let mut fortran = bytes[..10].to_vec();
197 fortran.extend_from_slice(header.as_bytes());
198 fortran.extend_from_slice(&bytes[10 + header_len..]);
199
200 assert_eq!(fortran.len(), bytes.len(), "only the flag should change");
201 let err = from_npy(&fortran).unwrap_err();
202 assert!(matches!(err, Error::Unsupported(_)), "{err}");
203 }
204}