Show More
@@ -11,6 +11,7 use crate::utils::hg_path::HgPath; | |||||
11 | use crate::utils::SliceExt; |
|
11 | use crate::utils::SliceExt; | |
12 | use crate::Graph; |
|
12 | use crate::Graph; | |
13 | use crate::GraphError; |
|
13 | use crate::GraphError; | |
|
14 | use crate::Node; | |||
14 | use crate::UncheckedRevision; |
|
15 | use crate::UncheckedRevision; | |
15 | use std::path::PathBuf; |
|
16 | use std::path::PathBuf; | |
16 |
|
17 | |||
@@ -216,22 +217,30 pub struct FilelogRevisionData(Vec<u8>); | |||||
216 |
|
217 | |||
217 | impl FilelogRevisionData { |
|
218 | impl FilelogRevisionData { | |
218 | /// Split into metadata and data |
|
219 | /// Split into metadata and data | |
219 | pub fn split(&self) -> Result<(Option<&[u8]>, &[u8]), HgError> { |
|
220 | pub fn split( | |
|
221 | &self, | |||
|
222 | ) -> Result<(FilelogRevisionMetadata<'_>, &[u8]), HgError> { | |||
220 | const DELIMITER: &[u8; 2] = b"\x01\n"; |
|
223 | const DELIMITER: &[u8; 2] = b"\x01\n"; | |
221 |
|
224 | |||
222 | if let Some(rest) = self.0.drop_prefix(DELIMITER) { |
|
225 | if let Some(rest) = self.0.drop_prefix(DELIMITER) { | |
223 | if let Some((metadata, data)) = rest.split_2_by_slice(DELIMITER) { |
|
226 | if let Some((metadata, data)) = rest.split_2_by_slice(DELIMITER) { | |
224 | Ok((Some(metadata), data)) |
|
227 | Ok((FilelogRevisionMetadata(Some(metadata)), data)) | |
225 | } else { |
|
228 | } else { | |
226 | Err(HgError::corrupted( |
|
229 | Err(HgError::corrupted( | |
227 | "Missing metadata end delimiter in filelog entry", |
|
230 | "Missing metadata end delimiter in filelog entry", | |
228 | )) |
|
231 | )) | |
229 | } |
|
232 | } | |
230 | } else { |
|
233 | } else { | |
231 | Ok((None, &self.0)) |
|
234 | Ok((FilelogRevisionMetadata(None), &self.0)) | |
232 | } |
|
235 | } | |
233 | } |
|
236 | } | |
234 |
|
237 | |||
|
238 | /// Returns the metadata header. | |||
|
239 | pub fn metadata(&self) -> Result<FilelogRevisionMetadata<'_>, HgError> { | |||
|
240 | let (metadata, _data) = self.split()?; | |||
|
241 | Ok(metadata) | |||
|
242 | } | |||
|
243 | ||||
235 | /// Returns the file contents at this revision, stripped of any metadata |
|
244 | /// Returns the file contents at this revision, stripped of any metadata | |
236 | pub fn file_data(&self) -> Result<&[u8], HgError> { |
|
245 | pub fn file_data(&self) -> Result<&[u8], HgError> { | |
237 | let (_metadata, data) = self.split()?; |
|
246 | let (_metadata, data) = self.split()?; | |
@@ -241,10 +250,138 impl FilelogRevisionData { | |||||
241 | /// Consume the entry, and convert it into data, discarding any metadata, |
|
250 | /// Consume the entry, and convert it into data, discarding any metadata, | |
242 | /// if present. |
|
251 | /// if present. | |
243 | pub fn into_file_data(self) -> Result<Vec<u8>, HgError> { |
|
252 | pub fn into_file_data(self) -> Result<Vec<u8>, HgError> { | |
244 |
if let (Some(_ |
|
253 | if let (FilelogRevisionMetadata(Some(_)), data) = self.split()? { | |
245 | Ok(data.to_owned()) |
|
254 | Ok(data.to_owned()) | |
246 | } else { |
|
255 | } else { | |
247 | Ok(self.0) |
|
256 | Ok(self.0) | |
248 | } |
|
257 | } | |
249 | } |
|
258 | } | |
250 | } |
|
259 | } | |
|
260 | ||||
|
261 | /// The optional metadata header included in [`FilelogRevisionData`]. | |||
|
262 | pub struct FilelogRevisionMetadata<'a>(Option<&'a [u8]>); | |||
|
263 | ||||
|
264 | /// Fields parsed from [`FilelogRevisionMetadata`]. | |||
|
265 | #[derive(Debug, PartialEq, Default)] | |||
|
266 | pub struct FilelogRevisionMetadataFields<'a> { | |||
|
267 | /// True if the file revision data is censored. | |||
|
268 | pub censored: bool, | |||
|
269 | /// Path of the copy source. | |||
|
270 | pub copy: Option<&'a HgPath>, | |||
|
271 | /// Filelog node ID of the copy source. | |||
|
272 | pub copyrev: Option<Node>, | |||
|
273 | } | |||
|
274 | ||||
|
275 | impl<'a> FilelogRevisionMetadata<'a> { | |||
|
276 | /// Parses the metadata fields. | |||
|
277 | pub fn parse(self) -> Result<FilelogRevisionMetadataFields<'a>, HgError> { | |||
|
278 | let mut fields = FilelogRevisionMetadataFields::default(); | |||
|
279 | if let Some(metadata) = self.0 { | |||
|
280 | let mut rest = metadata; | |||
|
281 | while !rest.is_empty() { | |||
|
282 | let Some(colon_idx) = memchr::memchr(b':', rest) else { | |||
|
283 | return Err(HgError::corrupted( | |||
|
284 | "File metadata header line missing colon", | |||
|
285 | )); | |||
|
286 | }; | |||
|
287 | if rest.get(colon_idx + 1) != Some(&b' ') { | |||
|
288 | return Err(HgError::corrupted( | |||
|
289 | "File metadata header line missing space", | |||
|
290 | )); | |||
|
291 | } | |||
|
292 | let key = &rest[..colon_idx]; | |||
|
293 | rest = &rest[colon_idx + 2..]; | |||
|
294 | let Some(newline_idx) = memchr::memchr(b'\n', rest) else { | |||
|
295 | return Err(HgError::corrupted( | |||
|
296 | "File metadata header line missing newline", | |||
|
297 | )); | |||
|
298 | }; | |||
|
299 | let value = &rest[..newline_idx]; | |||
|
300 | match key { | |||
|
301 | b"censored" => { | |||
|
302 | match value { | |||
|
303 | b"" => fields.censored = true, | |||
|
304 | _ => return Err(HgError::corrupted( | |||
|
305 | "File metadata header 'censored' field has nonempty value", | |||
|
306 | )), | |||
|
307 | } | |||
|
308 | } | |||
|
309 | b"copy" => fields.copy = Some(HgPath::new(value)), | |||
|
310 | b"copyrev" => { | |||
|
311 | fields.copyrev = Some(Node::from_hex_for_repo(value)?) | |||
|
312 | } | |||
|
313 | _ => { | |||
|
314 | return Err(HgError::corrupted( | |||
|
315 | format!( | |||
|
316 | "File metadata header has unrecognized key '{}'", | |||
|
317 | String::from_utf8_lossy(key), | |||
|
318 | ), | |||
|
319 | )) | |||
|
320 | } | |||
|
321 | } | |||
|
322 | rest = &rest[newline_idx + 1..]; | |||
|
323 | } | |||
|
324 | } | |||
|
325 | Ok(fields) | |||
|
326 | } | |||
|
327 | } | |||
|
328 | ||||
|
329 | #[cfg(test)] | |||
|
330 | mod tests { | |||
|
331 | use super::*; | |||
|
332 | use format_bytes::format_bytes; | |||
|
333 | ||||
|
334 | #[test] | |||
|
335 | fn test_parse_no_metadata() { | |||
|
336 | let data = FilelogRevisionData(b"data".to_vec()); | |||
|
337 | let fields = data.metadata().unwrap().parse().unwrap(); | |||
|
338 | assert_eq!(fields, Default::default()); | |||
|
339 | } | |||
|
340 | ||||
|
341 | #[test] | |||
|
342 | fn test_parse_empty_metadata() { | |||
|
343 | let data = FilelogRevisionData(b"\x01\n\x01\ndata".to_vec()); | |||
|
344 | let fields = data.metadata().unwrap().parse().unwrap(); | |||
|
345 | assert_eq!(fields, Default::default()); | |||
|
346 | } | |||
|
347 | ||||
|
348 | #[test] | |||
|
349 | fn test_parse_one_field() { | |||
|
350 | let data = | |||
|
351 | FilelogRevisionData(b"\x01\ncopy: foo\n\x01\ndata".to_vec()); | |||
|
352 | let fields = data.metadata().unwrap().parse().unwrap(); | |||
|
353 | assert_eq!( | |||
|
354 | fields, | |||
|
355 | FilelogRevisionMetadataFields { | |||
|
356 | copy: Some(HgPath::new("foo")), | |||
|
357 | ..Default::default() | |||
|
358 | } | |||
|
359 | ); | |||
|
360 | } | |||
|
361 | ||||
|
362 | #[test] | |||
|
363 | fn test_parse_all_fields() { | |||
|
364 | let sha = b"215d5d1546f82a79481eb2df513a7bc341bdf17f"; | |||
|
365 | let data = FilelogRevisionData(format_bytes!( | |||
|
366 | b"\x01\ncensored: \ncopy: foo\ncopyrev: {}\n\x01\ndata", | |||
|
367 | sha | |||
|
368 | )); | |||
|
369 | let fields = data.metadata().unwrap().parse().unwrap(); | |||
|
370 | assert_eq!( | |||
|
371 | fields, | |||
|
372 | FilelogRevisionMetadataFields { | |||
|
373 | censored: true, | |||
|
374 | copy: Some(HgPath::new("foo")), | |||
|
375 | copyrev: Some(Node::from_hex(sha).unwrap()), | |||
|
376 | } | |||
|
377 | ); | |||
|
378 | } | |||
|
379 | ||||
|
380 | #[test] | |||
|
381 | fn test_parse_invalid_metadata() { | |||
|
382 | let data = | |||
|
383 | FilelogRevisionData(b"\x01\nbad: value\n\x01\ndata".to_vec()); | |||
|
384 | let err = data.metadata().unwrap().parse().unwrap_err(); | |||
|
385 | assert!(err.to_string().contains("unrecognized key 'bad'")); | |||
|
386 | } | |||
|
387 | } |
General Comments 0
You need to be logged in to leave comments.
Login now