1use crate::{ProfileError, Result};
4use std::collections::BTreeMap;
5use zeroize::Zeroizing;
6
7pub struct ProfileIdentity {
8 pub username: String,
9 pub uin: u32,
10 pub device_id: Vec<u8>,
11}
12impl ProfileIdentity {
13 pub fn request_device_id(&self) -> Result<&[u8]> {
16 if !(16..=256).contains(&self.device_id.len()) {
17 return Err(ProfileError::InvalidMetadata);
18 }
19 Ok(&self.device_id[..16])
20 }
21}
22fn var(input: &[u8], at: &mut usize) -> Result<u64> {
23 let mut value = 0;
24 for i in 0..10 {
25 let byte = *input.get(*at).ok_or(ProfileError::InvalidMetadata)?;
26 *at += 1;
27 if i == 9 && byte > 1 {
28 return Err(ProfileError::InvalidMetadata);
29 }
30 value |= ((byte & 127) as u64) << (i * 7);
31 if byte & 128 == 0 {
32 return Ok(value);
33 }
34 }
35 Err(ProfileError::InvalidMetadata)
36}
37fn span<'a>(input: &'a [u8], at: &mut usize) -> Result<&'a [u8]> {
38 let n = usize::try_from(var(input, at)?).map_err(|_| ProfileError::ResourceLimit)?;
39 let end = at.checked_add(n).ok_or(ProfileError::ResourceLimit)?;
40 let value = input.get(*at..end).ok_or(ProfileError::InvalidMetadata)?;
41 *at = end;
42 Ok(value)
43}
44fn string(input: &[u8]) -> Result<Vec<u8>> {
45 let mut at = 0;
46 let value = span(input, &mut at)?.to_vec();
47 if at != input.len() {
48 return Err(ProfileError::InvalidMetadata);
49 }
50 Ok(value)
51}
52pub fn read_mmkv_identity(
55 data: &[u8],
56 metadata: &[u8],
57 format_key: &[u8],
58) -> Result<ProfileIdentity> {
59 if format_key.len() != 16
60 || data.len() < 4
61 || data.len() > 4 * 1024 * 1024 + 4
62 || metadata.len() < 40
63 || metadata.len() > 4096
64 {
65 return Err(ProfileError::InvalidMetadata);
66 }
67 let number = |offset| {
68 u32::from_le_bytes(
69 metadata[offset..offset + 4]
70 .try_into()
71 .expect("metadata bound"),
72 )
73 };
74 if number(4) != 4 || metadata.len() >= 108 && number(104) != 0 {
75 return Err(ProfileError::InvalidMetadata);
76 }
77 let actual = number(28) as usize;
78 let ciphertext = data
79 .get(4..4 + actual)
80 .ok_or(ProfileError::InvalidMetadata)?;
81 if crc32fast::hash(ciphertext) != number(0) {
82 return Err(ProfileError::InvalidMetadata);
83 }
84 let plain = Zeroizing::new(
85 openssl::symm::decrypt(
86 openssl::symm::Cipher::aes_128_cfb128(),
87 format_key,
88 Some(&metadata[12..28]),
89 ciphertext,
90 )
91 .map_err(|_| ProfileError::InvalidMetadata)?,
92 );
93 let mut at = 0;
94 var(&plain, &mut at)?;
95 let mut entries = BTreeMap::new();
96 let mut count = 0;
97 while at < plain.len() {
98 let key = std::str::from_utf8(span(&plain, &mut at)?)
99 .map_err(|_| ProfileError::InvalidMetadata)?;
100 if key.is_empty() || key.len() > 4096 || key.chars().any(char::is_control) {
101 return Err(ProfileError::InvalidMetadata);
102 }
103 let value = span(&plain, &mut at)?;
104 if value.is_empty() {
105 entries.remove(key);
106 } else {
107 entries.insert(key, value);
108 }
109 count += 1;
110 if count > 65536 {
111 return Err(ProfileError::ResourceLimit);
112 }
113 }
114 let get = |name: &str| {
115 entries
116 .get(name)
117 .copied()
118 .ok_or(ProfileError::InvalidMetadata)
119 };
120 let username = String::from_utf8(string(get("mmkv_key_user_name")?)?)
121 .map_err(|_| ProfileError::InvalidMetadata)?;
122 let mut at = 0;
123 let raw = get("mmkv_key_latest_login_uin")?;
124 let uin = u32::try_from(var(raw, &mut at)?).map_err(|_| ProfileError::InvalidMetadata)?;
125 if at != raw.len()
126 || uin == 0
127 || username.is_empty()
128 || username.len() > 256
129 || username.chars().any(char::is_control)
130 {
131 return Err(ProfileError::InvalidMetadata);
132 }
133 let device_id = string(get("key_wechat_device_id")?)?;
134 if device_id.len() < 16 || device_id.len() > 256 {
135 return Err(ProfileError::InvalidMetadata);
136 }
137 Ok(ProfileIdentity {
138 username,
139 uin,
140 device_id,
141 })
142}
143#[cfg(test)]
144mod tests {
145 use super::*;
146 fn append(value: u64, out: &mut Vec<u8>) {
147 let mut value = value;
148 while value >= 128 {
149 out.push((value as u8 & 127) | 128);
150 value >>= 7;
151 }
152 out.push(value as u8);
153 }
154 fn framed(value: &[u8]) -> Vec<u8> {
155 let mut out = vec![];
156 append(value.len() as u64, &mut out);
157 out.extend(value);
158 out
159 }
160 fn fixture() -> (Vec<u8>, Vec<u8>) {
161 let mut plain = vec![0];
162 for (key, value) in [
163 ("mmkv_key_user_name", framed(b"synthetic_self")),
164 ("mmkv_key_latest_login_uin", vec![7]),
165 ("key_wechat_device_id", framed(b"synthetic-device-16")),
166 ] {
167 plain.extend(framed(key.as_bytes()));
168 plain.extend(framed(&value));
169 }
170 let cipher = openssl::symm::encrypt(
171 openssl::symm::Cipher::aes_128_cfb128(),
172 &[9; 16],
173 Some(&[3; 16]),
174 &plain,
175 )
176 .unwrap();
177 let mut metadata = vec![0; 112];
178 metadata[..4].copy_from_slice(&crc32fast::hash(&cipher).to_le_bytes());
179 metadata[4..8].copy_from_slice(&4u32.to_le_bytes());
180 metadata[12..28].copy_from_slice(&[3; 16]);
181 metadata[28..32].copy_from_slice(&(cipher.len() as u32).to_le_bytes());
182 let mut data = vec![0; 4];
183 data.extend(cipher);
184 (data, metadata)
185 }
186 #[test]
187 fn identity_requires_consistent_version_crc_key_and_complete_scalar() {
188 let (mut data, mut metadata) = fixture();
189 let id = read_mmkv_identity(&data, &metadata, &[9; 16]).unwrap();
190 assert_eq!(id.username, "synthetic_self");
191 assert_eq!(id.uin, 7);
192 assert!(read_mmkv_identity(&data, &metadata, &[8; 16]).is_err());
193 data[4] ^= 1;
194 assert!(read_mmkv_identity(&data, &metadata, &[9; 16]).is_err());
195 data[4] ^= 1;
196 metadata[104] = 1;
197 assert!(read_mmkv_identity(&data, &metadata, &[9; 16]).is_err());
198 metadata[104] = 0;
199 metadata[4] = 5;
200 assert!(read_mmkv_identity(&data, &metadata, &[9; 16]).is_err());
201 }
202}
203
204pub struct MmkvRecords {
206 entries: BTreeMap<String, Zeroizing<Vec<u8>>>,
207 plain: Zeroizing<Vec<u8>>,
208}
209impl MmkvRecords {
210 pub fn raw_value(&self, name: &str) -> Option<&[u8]> {
211 self.entries.get(name).map(|v| v.as_slice())
212 }
213 pub fn string_value(&self, name: &str) -> Result<Option<Zeroizing<Vec<u8>>>> {
214 self.raw_value(name)
215 .map(string)
216 .transpose()
217 .map(|v| v.map(Zeroizing::new))
218 }
219 pub fn contains(&self, name: &str) -> bool {
220 self.entries.contains_key(name)
221 }
222 pub fn len(&self) -> usize {
223 self.entries.len()
224 }
225 pub fn is_empty(&self) -> bool {
226 self.entries.is_empty()
227 }
228}
229fn append_var(mut value: u64, out: &mut Vec<u8>) {
230 while value >= 128 {
231 out.push((value as u8 & 127) | 128);
232 value >>= 7;
233 }
234 out.push(value as u8)
235}
236fn append_span(value: &[u8], out: &mut Vec<u8>) {
237 append_var(value.len() as u64, out);
238 out.extend(value)
239}
240pub fn mmkv_string_value(value: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
242 if value.len() > 1024 * 1024 {
243 return Err(ProfileError::ResourceLimit);
244 }
245 let mut output = Zeroizing::new(Vec::new());
246 append_span(value, &mut output);
247 Ok(output)
248}
249pub fn decode_mmkv_records(data: &[u8], metadata: &[u8], key: &[u8]) -> Result<MmkvRecords> {
250 if key.len() != 16
251 || data.len() < 4
252 || data.len() > 4 * 1024 * 1024 + 4
253 || metadata.len() < 112
254 || metadata.len() > 4096
255 {
256 return Err(ProfileError::InvalidMetadata);
257 }
258 let n = |o| u32::from_le_bytes(metadata[o..o + 4].try_into().expect("bounded"));
259 if n(4) != 4 || metadata[104..112].iter().any(|b| *b != 0) {
260 return Err(ProfileError::InvalidMetadata);
261 }
262 let length = n(28) as usize;
263 let cipher = data
264 .get(4..4 + length)
265 .ok_or(ProfileError::InvalidMetadata)?;
266 if crc32fast::hash(cipher) != n(0) {
267 return Err(ProfileError::InvalidMetadata);
268 }
269 let plain = Zeroizing::new(
270 openssl::symm::decrypt(
271 openssl::symm::Cipher::aes_128_cfb128(),
272 key,
273 Some(&metadata[12..28]),
274 cipher,
275 )
276 .map_err(|_| ProfileError::InvalidMetadata)?,
277 );
278 let mut at = 0;
279 var(&plain, &mut at)?;
280 let mut entries = BTreeMap::new();
281 let mut records = 0;
282 while at < plain.len() {
283 let name = std::str::from_utf8(span(&plain, &mut at)?)
284 .map_err(|_| ProfileError::InvalidMetadata)?;
285 if name.is_empty() || name.len() > 4096 || name.chars().any(char::is_control) {
286 return Err(ProfileError::InvalidMetadata);
287 }
288 let value = span(&plain, &mut at)?;
289 if value.is_empty() {
290 entries.remove(name);
291 } else {
292 entries.insert(name.to_owned(), Zeroizing::new(value.to_vec()));
293 }
294 records += 1;
295 if records > 65536 {
296 return Err(ProfileError::ResourceLimit);
297 }
298 }
299 Ok(MmkvRecords { entries, plain })
300}
301pub struct StagedMmkv {
304 data: Zeroizing<Vec<u8>>,
305 metadata: Vec<u8>,
306 pub changed_keys: Vec<String>,
307 expected_data_sha256: Option<[u8; 32]>,
308 expected_meta_sha256: Option<[u8; 32]>,
309}
310impl StagedMmkv {
311 pub fn data(&self) -> &[u8] {
312 &self.data
313 }
314 pub fn metadata(&self) -> &[u8] {
315 &self.metadata
316 }
317}
318pub fn stage_mmkv_values(
323 data: &[u8],
324 metadata: &[u8],
325 key: &[u8],
326 updates: &[(String, Option<Vec<u8>>)],
327) -> Result<StagedMmkv> {
328 let records = decode_mmkv_records(data, metadata, key)?;
329 let mut plain = records.plain.clone();
330 let mut changed = vec![];
331 let mut names = std::collections::BTreeSet::new();
332 for (name, value) in updates {
333 if name.is_empty()
334 || name.len() > 4096
335 || name.chars().any(char::is_control)
336 || !names.insert(name)
337 {
338 return Err(ProfileError::InvalidMetadata);
339 }
340 if value
341 .as_ref()
342 .is_some_and(|v| v.is_empty() || v.len() > 1024 * 1024)
343 {
344 return Err(ProfileError::InvalidMetadata);
345 }
346 if records.raw_value(name) == value.as_deref() {
347 continue;
348 }
349 append_span(name.as_bytes(), &mut plain);
350 append_span(value.as_deref().unwrap_or_default(), &mut plain);
351 if plain.len() + 4 > data.len().max(4 * 1024 * 1024) {
352 return Err(ProfileError::ResourceLimit);
353 }
354 changed.push(name.clone());
355 }
356 let capacity = if plain.len() > data.len() - 4 {
357 let rounded = (plain.len() + 4).div_ceil(4096) * 4096;
358 if rounded > 4 * 1024 * 1024 {
359 return Err(ProfileError::ResourceLimit);
360 }
361 rounded
362 } else {
363 data.len()
364 };
365 let cipher = Zeroizing::new(
366 openssl::symm::encrypt(
367 openssl::symm::Cipher::aes_128_cfb128(),
368 key,
369 Some(&metadata[12..28]),
370 &plain,
371 )
372 .map_err(|_| ProfileError::InvalidMetadata)?,
373 );
374 let old = u32::from_le_bytes(metadata[28..32].try_into().expect("bounded")) as usize;
375 if cipher.get(..old) != data.get(4..4 + old) {
376 return Err(ProfileError::InvalidMetadata);
377 }
378 let mut output = Zeroizing::new(data.to_vec());
379 output.resize(capacity, 0);
380 output[4..4 + cipher.len()].copy_from_slice(&cipher);
381 let mut meta = metadata.to_vec();
382 meta[..4].copy_from_slice(&crc32fast::hash(&cipher).to_le_bytes());
383 meta[28..32].copy_from_slice(&(cipher.len() as u32).to_le_bytes());
384 let checked = decode_mmkv_records(&output, &meta, key)?;
385 for (name, value) in updates {
386 if checked.raw_value(name) != value.as_deref() {
387 return Err(ProfileError::InvalidMetadata);
388 }
389 }
390 Ok(StagedMmkv {
391 data: output,
392 metadata: meta,
393 changed_keys: changed,
394 expected_data_sha256: Some(mmkv_hash(data)),
395 expected_meta_sha256: Some(mmkv_hash(metadata)),
396 })
397}
398pub fn create_mmkv_v4(key: &[u8], iv: [u8; 16], capacity: usize) -> Result<StagedMmkv> {
403 if key.len() != 16
404 || !(4096..=4 * 1024 * 1024).contains(&capacity)
405 || !capacity.is_multiple_of(4096)
406 {
407 return Err(ProfileError::InvalidMetadata);
408 }
409 let cipher = openssl::symm::encrypt(
410 openssl::symm::Cipher::aes_128_cfb128(),
411 key,
412 Some(&iv),
413 &[0],
414 )
415 .map_err(|_| ProfileError::InvalidMetadata)?;
416 let mut data = Zeroizing::new(vec![0; capacity]);
417 data[4] = cipher[0];
418 let mut meta = vec![0; 4096];
419 meta[..4].copy_from_slice(&crc32fast::hash(&cipher).to_le_bytes());
420 meta[4..8].copy_from_slice(&4u32.to_le_bytes());
421 meta[12..28].copy_from_slice(&iv);
422 meta[28..32].copy_from_slice(&1u32.to_le_bytes());
423 Ok(StagedMmkv {
424 data,
425 metadata: meta,
426 changed_keys: vec![],
427 expected_data_sha256: None,
428 expected_meta_sha256: None,
429 })
430}
431pub fn stage_mmkv_sync_key(
434 data: &[u8],
435 metadata: &[u8],
436 key: &[u8],
437 sync_key: &[u8],
438) -> Result<StagedMmkv> {
439 if sync_key.is_empty() || sync_key.len() > 1024 * 1024 {
440 return Err(ProfileError::InvalidMetadata);
441 }
442 let value = mmkv_string_value(sync_key)?;
443 stage_mmkv_values(
444 data,
445 metadata,
446 key,
447 &[("mmkv_key_sync_key".into(), Some(value.to_vec()))],
448 )
449}
450
451fn mmkv_hash(bytes: &[u8]) -> [u8; 32] {
452 use sha2::{Digest, Sha256};
453 Sha256::digest(bytes).into()
454}
455#[derive(Clone, Debug, serde::Serialize)]
456#[serde(rename_all = "camelCase")]
457pub struct MmkvPublication {
458 pub data_replaced: bool,
459 pub metadata_replaced: bool,
460 pub already_applied: bool,
461}
462fn safe_pair_path(path: &std::path::Path) -> Result<std::path::PathBuf> {
463 let parent = path.parent().ok_or(ProfileError::InvalidQuery)?;
464 let canonical = std::fs::canonicalize(parent)?;
465 if canonical != parent {
466 return Err(ProfileError::UnsupportedFile);
467 }
468 let name = path.file_name().ok_or(ProfileError::InvalidQuery)?;
469 let target = canonical.join(name);
470 for p in [
471 &target,
472 &target.with_file_name(format!("{}.crc", name.to_string_lossy())),
473 ] {
474 let m = std::fs::symlink_metadata(p)?;
475 if !m.is_file() || wechat_platform::is_link(&m) {
476 return Err(ProfileError::UnsupportedFile);
477 }
478 }
479 Ok(target)
480}
481fn replace_mmkv_file(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
482 use std::io::Write;
483 let parent = path.parent().ok_or(ProfileError::InvalidQuery)?;
484 let mut staged = wechat_platform::private_tempfile_in(parent, ".wx-mmkv-")?;
485 staged.write_all(bytes)?;
486 staged.as_file().sync_all()?;
487 staged
488 .persist(path)
489 .map_err(|e| ProfileError::Io(e.error))?;
490 wechat_platform::sync_dir(parent)?;
491 Ok(())
492}
493pub fn publish_mmkv_append(
499 path: impl AsRef<std::path::Path>,
500 staged: &StagedMmkv,
501 options: &crate::WriteOptions,
502) -> Result<MmkvPublication> {
503 publish_mmkv_append_guarded(path, staged, options, || true)
504}
505pub fn publish_mmkv_append_guarded(
509 path: impl AsRef<std::path::Path>,
510 staged: &StagedMmkv,
511 options: &crate::WriteOptions,
512 mut allowed: impl FnMut() -> bool,
513) -> Result<MmkvPublication> {
514 if !options.exclusive_ownership_confirmed {
515 return Err(ProfileError::SnapshotRequired);
516 }
517 if options.durable_intent_id.is_empty() || options.durable_intent_id.len() > 256 {
518 return Err(ProfileError::InvalidQuery);
519 }
520 let old_data = staged
521 .expected_data_sha256
522 .ok_or(ProfileError::InvalidMetadata)?;
523 let old_meta = staged
524 .expected_meta_sha256
525 .ok_or(ProfileError::InvalidMetadata)?;
526 let path = safe_pair_path(path.as_ref())?;
527 let meta = path.with_file_name(format!(
528 "{}.crc",
529 path.file_name()
530 .ok_or(ProfileError::InvalidQuery)?
531 .to_string_lossy()
532 ));
533 let current_data = mmkv_hash(&std::fs::read(&path)?);
534 let current_meta = mmkv_hash(&std::fs::read(&meta)?);
535 let new_data = mmkv_hash(staged.data());
536 let new_meta = mmkv_hash(staged.metadata());
537 if current_data == new_data && current_meta == new_meta {
538 return Ok(MmkvPublication {
539 data_replaced: false,
540 metadata_replaced: false,
541 already_applied: true,
542 });
543 }
544 if current_meta != old_meta || (current_data != old_data && current_data != new_data) {
545 return Err(ProfileError::SourceChanged);
546 }
547 let replace_data = current_data != new_data;
548 if replace_data {
549 if !allowed() {
550 return Err(ProfileError::SourceChanged);
551 }
552 replace_mmkv_file(&path, staged.data())?;
553 }
554 if mmkv_hash(&std::fs::read(&meta)?) != old_meta
555 || mmkv_hash(&std::fs::read(&path)?) != new_data
556 {
557 return Err(ProfileError::SourceChanged);
558 }
559 if !allowed() {
560 return Err(ProfileError::SourceChanged);
561 }
562 replace_mmkv_file(&meta, staged.metadata())?;
563 Ok(MmkvPublication {
564 data_replaced: replace_data,
565 metadata_replaced: true,
566 already_applied: false,
567 })
568}
569
570pub fn account_mmkv_key(username: &str) -> Result<Zeroizing<Vec<u8>>> {
574 use md5::{Digest, Md5};
575 crate::projection::identity(username)?;
576 Ok(Zeroizing::new(Md5::digest(username.as_bytes()).to_vec()))
577}
578pub fn account_sync_store_path(
580 location: &crate::ProfileLocation,
581 identity: &ProfileIdentity,
582) -> Result<std::path::PathBuf> {
583 if location.account_directory()
584 != crate::official_account_directory(&identity.username, identity.uin)?
585 {
586 return Err(ProfileError::AccountMismatch);
587 }
588 Ok(location.root().join("config/login_configv2"))
589}
590pub fn account_contact_sync_store_path(
593 location: &crate::ProfileLocation,
594 identity: &ProfileIdentity,
595) -> Result<std::path::PathBuf> {
596 account_sync_store_path(location, identity)?;
597 Ok(location.root().join("config/rs_config"))
598}
599pub fn stage_account_contact_sync_key(
600 data: &[u8],
601 metadata: &[u8],
602 username: &str,
603 cursor: &[u8],
604) -> Result<StagedMmkv> {
605 if cursor.len() > 65536 {
606 return Err(ProfileError::ResourceLimit);
607 }
608 let key = account_mmkv_key(username)?;
609 stage_mmkv_values(
610 data,
611 metadata,
612 &key,
613 &[("sync_11".into(), Some(mmkv_string_value(cursor)?.to_vec()))],
614 )
615}
616pub fn read_account_sync_key(
619 location: &crate::ProfileLocation,
620 identity: &ProfileIdentity,
621 options: &crate::ReadOptions,
622) -> Result<Zeroizing<Vec<u8>>> {
623 if !options.stopped_or_consistent_snapshot {
624 return Err(ProfileError::SnapshotRequired);
625 }
626 account_sync_store_path(location, identity)?;
627 let data = crate::media::read_cache(
628 location.root(),
629 std::path::Path::new("config/login_configv2"),
630 4 * 1024 * 1024,
631 )?;
632 let metadata = crate::media::read_cache(
633 location.root(),
634 std::path::Path::new("config/login_configv2.crc"),
635 4096,
636 )?;
637 let key = account_mmkv_key(&identity.username)?;
638 let records = decode_mmkv_records(&data, &metadata, &key)?;
639 Ok(records
640 .string_value("mmkv_key_sync_key")?
641 .unwrap_or_default())
642}
643pub fn stage_account_sync_key(
646 data: &[u8],
647 metadata: &[u8],
648 username: &str,
649 cursor: &[u8],
650) -> Result<StagedMmkv> {
651 let key = account_mmkv_key(username)?;
652 stage_mmkv_sync_key(data, metadata, &key, cursor)
653}
654
655pub fn publish_mmkv_create(
660 path: impl AsRef<std::path::Path>,
661 staged: &StagedMmkv,
662 options: &crate::WriteOptions,
663) -> Result<MmkvPublication> {
664 use std::{fs, io::Write, path::Path};
665 if !options.exclusive_ownership_confirmed {
666 return Err(ProfileError::SnapshotRequired);
667 }
668 if options.durable_intent_id.is_empty() || options.durable_intent_id.len() > 256 {
669 return Err(ProfileError::InvalidQuery);
670 }
671 if staged.expected_data_sha256.is_some() || staged.expected_meta_sha256.is_some() {
672 return Err(ProfileError::InvalidMetadata);
673 }
674 let path = path.as_ref();
675 let parent = path.parent().ok_or(ProfileError::InvalidQuery)?;
676 if fs::canonicalize(parent)? != parent {
677 return Err(ProfileError::UnsupportedFile);
678 }
679 let meta = path.with_file_name(format!(
680 "{}.crc",
681 path.file_name()
682 .and_then(|n| n.to_str())
683 .ok_or(ProfileError::InvalidQuery)?
684 ));
685 fn exists_exact(path: &Path, bytes: &[u8]) -> Result<bool> {
686 match fs::symlink_metadata(path) {
687 Ok(m) => {
688 if !m.is_file() || wechat_platform::is_link(&m) {
689 return Err(ProfileError::UnsupportedFile);
690 }
691 if m.len() != bytes.len() as u64 {
692 return Err(ProfileError::SourceChanged);
693 }
694 let old = fs::read(path)?;
695 if old != bytes {
696 return Err(ProfileError::SourceChanged);
697 }
698 Ok(true)
699 }
700 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
701 Err(e) => Err(e.into()),
702 }
703 }
704 let data_exists = exists_exact(path, staged.data())?;
705 let meta_exists = exists_exact(&meta, staged.metadata())?;
706 fn create(path: &Path, bytes: &[u8]) -> Result<()> {
707 let parent = path.parent().ok_or(ProfileError::InvalidQuery)?;
708 let mut temp = wechat_platform::private_tempfile_in(parent, ".wx-mmkv-")?;
709 temp.write_all(bytes)?;
710 temp.as_file().sync_all()?;
711 temp.persist_noclobber(path)
712 .map_err(|e| ProfileError::Io(e.error))?;
713 wechat_platform::sync_dir(parent)?;
714 Ok(())
715 }
716 if !data_exists {
717 create(path, staged.data())?;
718 }
719 if !meta_exists {
720 create(&meta, staged.metadata())?;
721 }
722 Ok(MmkvPublication {
723 data_replaced: !data_exists,
724 metadata_replaced: !meta_exists,
725 already_applied: data_exists && meta_exists,
726 })
727}