1//! All binary files generated by measureme have a simple file header that
2//! consists of a 4 byte file magic string and a 4 byte little-endian version
3//! number.
4use std::convert::TryInto;
5use std::error::Error;
6use std::path::Path;
7
8pub const CURRENT_FILE_FORMAT_VERSION: u32 = 9;
9
10pub const FILE_MAGIC_TOP_LEVEL: &[u8; 4] = b"MMPD";
11pub const FILE_MAGIC_EVENT_STREAM: &[u8; 4] = b"MMES";
12pub const FILE_MAGIC_STRINGTABLE_DATA: &[u8; 4] = b"MMSD";
13pub const FILE_MAGIC_STRINGTABLE_INDEX: &[u8; 4] = b"MMSI";
14
15pub const FILE_EXTENSION: &str = "mm_profdata";
16
17/// The size of the file header in bytes. Note that functions in this module
18/// rely on this size to be `8`.
19pub const FILE_HEADER_SIZE: usize = 8;
20
21pub fn write_file_header(
22 s: &mut dyn std::io::Write,
23 file_magic: &[u8; 4],
24) -> Result<(), Box<dyn Error + Send + Sync>> {
25 // The implementation here relies on FILE_HEADER_SIZE to have the value 8.
26 // Let's make sure this assumption cannot be violated without being noticed.
27 assert_eq!(FILE_HEADER_SIZE, 8);
28
29 s.write_all(file_magic).map_err(op:Box::new)?;
30 s.write_all(&CURRENT_FILE_FORMAT_VERSION.to_le_bytes())
31 .map_err(op:Box::new)?;
32
33 Ok(())
34}
35
36#[must_use]
37pub fn verify_file_header(
38 bytes: &[u8],
39 expected_magic: &[u8; 4],
40 diagnostic_file_path: Option<&Path>,
41 stream_tag: &str,
42) -> Result<(), Box<dyn Error + Send + Sync>> {
43 // The implementation here relies on FILE_HEADER_SIZE to have the value 8.
44 // Let's make sure this assumption cannot be violated without being noticed.
45 assert_eq!(FILE_HEADER_SIZE, 8);
46
47 let diagnostic_file_path = diagnostic_file_path.unwrap_or(Path::new("<in-memory>"));
48
49 if bytes.len() < FILE_HEADER_SIZE {
50 let msg = format!(
51 "Error reading {} stream in file `{}`: Expected file to contain at least `{:?}` bytes but found `{:?}` bytes",
52 stream_tag,
53 diagnostic_file_path.display(),
54 FILE_HEADER_SIZE,
55 bytes.len()
56 );
57
58 return Err(From::from(msg));
59 }
60
61 let actual_magic = &bytes[0..4];
62
63 if actual_magic != expected_magic {
64 let msg = format!(
65 "Error reading {} stream in file `{}`: Expected file magic `{:?}` but found `{:?}`",
66 stream_tag,
67 diagnostic_file_path.display(),
68 expected_magic,
69 actual_magic
70 );
71
72 return Err(From::from(msg));
73 }
74
75 let file_format_version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
76
77 if file_format_version != CURRENT_FILE_FORMAT_VERSION {
78 let msg = format!(
79 "Error reading {} stream in file `{}`: Expected file format version {} but found `{}`",
80 stream_tag,
81 diagnostic_file_path.display(),
82 CURRENT_FILE_FORMAT_VERSION,
83 file_format_version
84 );
85
86 return Err(From::from(msg));
87 }
88
89 Ok(())
90}
91
92pub fn strip_file_header(data: &[u8]) -> &[u8] {
93 &data[FILE_HEADER_SIZE..]
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use crate::{PageTag, SerializationSinkBuilder};
100
101 #[test]
102 fn roundtrip() {
103 let data_sink = SerializationSinkBuilder::new_in_memory().new_sink(PageTag::Events);
104
105 write_file_header(&mut data_sink.as_std_write(), FILE_MAGIC_EVENT_STREAM).unwrap();
106
107 let data = data_sink.into_bytes();
108
109 verify_file_header(&data, FILE_MAGIC_EVENT_STREAM, None, "test").unwrap();
110 }
111
112 #[test]
113 fn invalid_magic() {
114 let data_sink = SerializationSinkBuilder::new_in_memory().new_sink(PageTag::Events);
115 write_file_header(&mut data_sink.as_std_write(), FILE_MAGIC_STRINGTABLE_DATA).unwrap();
116 let mut data = data_sink.into_bytes();
117
118 // Invalidate the filemagic
119 data[2] = 0;
120 assert!(verify_file_header(&data, FILE_MAGIC_STRINGTABLE_DATA, None, "test").is_err());
121 }
122
123 #[test]
124 fn other_version() {
125 let data_sink = SerializationSinkBuilder::new_in_memory().new_sink(PageTag::Events);
126
127 write_file_header(&mut data_sink.as_std_write(), FILE_MAGIC_STRINGTABLE_INDEX).unwrap();
128
129 let mut data = data_sink.into_bytes();
130
131 // Change version
132 data[4] = 0xFF;
133 data[5] = 0xFF;
134 data[6] = 0xFF;
135 data[7] = 0xFF;
136 assert!(verify_file_header(&data, FILE_MAGIC_STRINGTABLE_INDEX, None, "test").is_err());
137 }
138
139 #[test]
140 fn empty_file() {
141 let data: [u8; 0] = [];
142
143 assert!(verify_file_header(&data, FILE_MAGIC_STRINGTABLE_DATA, None, "test").is_err());
144 }
145}
146