bedrock_world/
level_dat.rs1use crate::error::{BedrockWorldError, Result};
8use crate::nbt::{NbtTag, nbt_tags_equal_for_write, parse_root_nbt, serialize_root_nbt};
9use std::fs;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13const MAX_LEVEL_DAT_PAYLOAD_BYTES: usize = u32::MAX as usize;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct LevelDatHeader {
18 pub version: u32,
20 pub declared_len: u32,
22 pub actual_payload_len: usize,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum LevelDatReadWarning {
29 DeclaredLengthTooLarge {
31 declared_len: u32,
33 actual_payload_len: usize,
35 },
36 TrailingBytes {
38 declared_len: u32,
40 actual_payload_len: usize,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq)]
46pub struct LevelDatDocument {
48 pub header: LevelDatHeader,
50 pub root: NbtTag,
52 pub warnings: Vec<LevelDatReadWarning>,
54}
55
56impl LevelDatDocument {
57 #[must_use]
58 pub fn new(version: u32, root: NbtTag) -> Self {
60 Self {
61 header: LevelDatHeader {
62 version,
63 declared_len: 0,
64 actual_payload_len: 0,
65 },
66 root,
67 warnings: Vec::new(),
68 }
69 }
70
71 #[must_use]
72 pub const fn version(&self) -> u32 {
74 self.header.version
75 }
76}
77
78pub fn parse_level_dat_document(data: &[u8]) -> Result<LevelDatDocument> {
80 if data.len() < 8 {
81 return Err(BedrockWorldError::CorruptWorld(
82 "level.dat is shorter than its 8-byte header".to_string(),
83 ));
84 }
85
86 let version = read_header_u32(data, 0)?;
87 let declared_len = read_header_u32(data, 4)?;
88 let remaining = data.len().saturating_sub(8);
89 let declared_len_usize = declared_len as usize;
90
91 let mut warnings = Vec::new();
92 let payload = if declared_len_usize <= remaining {
93 if declared_len_usize < remaining {
94 warnings.push(LevelDatReadWarning::TrailingBytes {
95 declared_len,
96 actual_payload_len: remaining,
97 });
98 }
99 &data[8..8 + declared_len_usize]
100 } else {
101 warnings.push(LevelDatReadWarning::DeclaredLengthTooLarge {
102 declared_len,
103 actual_payload_len: remaining,
104 });
105 &data[8..]
106 };
107
108 let root = parse_root_nbt(payload)?;
109 Ok(LevelDatDocument {
110 header: LevelDatHeader {
111 version,
112 declared_len,
113 actual_payload_len: payload.len(),
114 },
115 root,
116 warnings,
117 })
118}
119
120pub fn read_level_dat_document(path: &Path) -> Result<LevelDatDocument> {
122 let bytes = fs::read(path)?;
123 parse_level_dat_document(&bytes)
124}
125
126pub fn read_level_dat(path: &Path) -> Result<LevelDatDocument> {
128 read_level_dat_document(path)
129}
130
131pub fn write_level_dat_document(path: &Path, document: &LevelDatDocument) -> Result<()> {
133 if path.file_name().is_some_and(|name| name != "level.dat") {
134 return Err(BedrockWorldError::Validation(format!(
135 "refusing to write non-level.dat file: {}",
136 path.display()
137 )));
138 }
139 if let Some(parent) = path.parent() {
140 if !parent.is_dir() {
141 return Err(BedrockWorldError::Validation(format!(
142 "level.dat parent directory does not exist: {}",
143 parent.display()
144 )));
145 }
146 }
147
148 let payload = serialize_root_nbt(&document.root)?;
149 if payload.len() > MAX_LEVEL_DAT_PAYLOAD_BYTES {
150 return Err(BedrockWorldError::Validation(
151 "level.dat payload is too large".to_string(),
152 ));
153 }
154
155 let mut bytes = Vec::with_capacity(payload.len() + 8);
156 bytes.extend_from_slice(&document.header.version.to_le_bytes());
157 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
158 bytes.extend_from_slice(&payload);
159 validate_level_dat_bytes_for_write(&bytes, &document.root, document.header.version)?;
160
161 let temporary_path = temporary_level_dat_path(path);
162 let mut file = fs::File::create(&temporary_path)?;
163 file.write_all(&bytes)?;
164 file.sync_all()?;
165 drop(file);
166
167 replace_file(&temporary_path, path)
168}
169
170pub fn write_level_dat_atomic(path: &Path, document: &LevelDatDocument) -> Result<()> {
172 write_level_dat_document(path, document)
173}
174
175#[cfg(feature = "async")]
176pub async fn read_level_dat_async(path: impl AsRef<Path>) -> Result<LevelDatDocument> {
178 let path = path.as_ref().to_path_buf();
179 tokio::task::spawn_blocking(move || read_level_dat(&path))
180 .await
181 .map_err(|error| BedrockWorldError::Join(error.to_string()))?
182}
183
184#[cfg(feature = "async")]
185pub async fn write_level_dat_atomic_async(
187 path: impl AsRef<Path>,
188 document: LevelDatDocument,
189) -> Result<()> {
190 let path = path.as_ref().to_path_buf();
191 tokio::task::spawn_blocking(move || write_level_dat_atomic(&path, &document))
192 .await
193 .map_err(|error| BedrockWorldError::Join(error.to_string()))?
194}
195
196pub fn validate_level_dat_bytes_for_write(
198 bytes: &[u8],
199 expected_root: &NbtTag,
200 expected_version: u32,
201) -> Result<()> {
202 let parsed = parse_level_dat_document(bytes)?;
203 if parsed.header.version != expected_version {
204 return Err(BedrockWorldError::Validation(
205 "level.dat version changed during write validation".to_string(),
206 ));
207 }
208 if parsed.header.declared_len as usize != bytes.len().saturating_sub(8) {
209 return Err(BedrockWorldError::Validation(
210 "level.dat declared length does not match payload".to_string(),
211 ));
212 }
213 if !parsed.warnings.is_empty() {
214 return Err(BedrockWorldError::Validation(format!(
215 "level.dat validation produced warnings: {:?}",
216 parsed.warnings
217 )));
218 }
219 if !nbt_tags_equal_for_write(&parsed.root, expected_root) {
220 return Err(BedrockWorldError::Validation(
221 "level.dat roundtrip root mismatch".to_string(),
222 ));
223 }
224 Ok(())
225}
226
227fn read_header_u32(data: &[u8], offset: usize) -> Result<u32> {
228 let bytes = data.get(offset..offset + 4).ok_or_else(|| {
229 BedrockWorldError::CorruptWorld("level.dat header is incomplete".to_string())
230 })?;
231 let bytes: [u8; 4] = bytes
232 .try_into()
233 .map_err(|_| BedrockWorldError::CorruptWorld("invalid level.dat header".to_string()))?;
234 Ok(u32::from_le_bytes(bytes))
235}
236
237fn temporary_level_dat_path(path: &Path) -> PathBuf {
238 path.with_file_name("level.dat.bmcbtmp")
239}
240
241fn replace_file(source: &Path, target: &Path) -> Result<()> {
242 replace_file_impl(source, target)
243}
244
245fn replace_file_impl(source: &Path, target: &Path) -> Result<()> {
246 if fs::rename(source, target).is_ok() {
247 return Ok(());
248 }
249 if target.exists() {
250 fs::remove_file(target)?;
251 }
252 fs::rename(source, target)?;
253 Ok(())
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::nbt::NbtTag;
260 use indexmap::IndexMap;
261
262 #[test]
263 fn level_dat_header_roundtrips() {
264 let mut root = IndexMap::new();
265 root.insert("LevelName".to_string(), NbtTag::String("Test".to_string()));
266 let root = NbtTag::Compound(root);
267 let payload = serialize_root_nbt(&root).expect("serialize");
268
269 let mut bytes = Vec::new();
270 bytes.extend_from_slice(&10_u32.to_le_bytes());
271 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
272 bytes.extend_from_slice(&payload);
273
274 let document = parse_level_dat_document(&bytes).expect("parse");
275 assert_eq!(document.header.version, 10);
276 assert_eq!(document.header.actual_payload_len, payload.len());
277 assert!(document.warnings.is_empty());
278 }
279
280 #[test]
281 fn level_dat_warns_when_declared_length_is_too_large() {
282 let root = NbtTag::Compound(IndexMap::new());
283 let payload = serialize_root_nbt(&root).expect("serialize");
284
285 let mut bytes = Vec::new();
286 bytes.extend_from_slice(&10_u32.to_le_bytes());
287 bytes.extend_from_slice(&((payload.len() + 8) as u32).to_le_bytes());
288 bytes.extend_from_slice(&payload);
289
290 let document = parse_level_dat_document(&bytes).expect("parse");
291 assert_eq!(
292 document.warnings,
293 vec![LevelDatReadWarning::DeclaredLengthTooLarge {
294 declared_len: (payload.len() + 8) as u32,
295 actual_payload_len: payload.len(),
296 }]
297 );
298 }
299}