stateman 2.0.0__tar.gz → 2.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stateman
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: This module provides tools to track changes in the content of a directory and create compact binary patches that can be used to synchronize another copy of that directory.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: vpuhoff
|
|
@@ -61,6 +61,9 @@ Starting with v2.0, patches are safe to apply even when they come from an untrus
|
|
|
61
61
|
|
|
62
62
|
## Changelog
|
|
63
63
|
|
|
64
|
+
### 2.0.1
|
|
65
|
+
* Fixed #4: a failure mid-extraction (corrupted ZIP stream, hash mismatch) could leave a partially written file in the target directory after rollback. Files are now extracted to a temporary file, hash-verified, and atomically moved into place (`os.replace`), so a partial file can never appear at the target path.
|
|
66
|
+
|
|
64
67
|
### 2.0.0
|
|
65
68
|
* **Breaking:** `ApplyPatch` now raises `PatchVerificationError` when the final state hash does not match the patch target state (previously it printed a warning and returned `True`), and when a file extracted from the patch fails its hash check.
|
|
66
69
|
* **Breaking:** default hash algorithm switched from MD5 to SHA-256. Legacy v1 (MD5) patches are still applied correctly.
|
|
@@ -44,6 +44,9 @@ Starting with v2.0, patches are safe to apply even when they come from an untrus
|
|
|
44
44
|
|
|
45
45
|
## Changelog
|
|
46
46
|
|
|
47
|
+
### 2.0.1
|
|
48
|
+
* Fixed #4: a failure mid-extraction (corrupted ZIP stream, hash mismatch) could leave a partially written file in the target directory after rollback. Files are now extracted to a temporary file, hash-verified, and atomically moved into place (`os.replace`), so a partial file can never appear at the target path.
|
|
49
|
+
|
|
47
50
|
### 2.0.0
|
|
48
51
|
* **Breaking:** `ApplyPatch` now raises `PatchVerificationError` when the final state hash does not match the patch target state (previously it printed a warning and returned `True`), and when a file extracted from the patch fails its hash check.
|
|
49
52
|
* **Breaking:** default hash algorithm switched from MD5 to SHA-256. Legacy v1 (MD5) patches are still applied correctly.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "stateman"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.1"
|
|
4
4
|
description = "This module provides tools to track changes in the content of a directory and create compact binary patches that can be used to synchronize another copy of that directory."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -387,6 +387,10 @@ def ApplyPatch(target, patch_file, exclude=None,
|
|
|
387
387
|
backed_up = {}
|
|
388
388
|
created = []
|
|
389
389
|
total_extracted = 0
|
|
390
|
+
# Current umask, used to give extracted files the same permissions
|
|
391
|
+
# they would get from a plain open(path, "wb") (mkstemp uses 0600).
|
|
392
|
+
umask = os.umask(0)
|
|
393
|
+
os.umask(umask)
|
|
390
394
|
|
|
391
395
|
def _rollback():
|
|
392
396
|
"""Restore the target directory to its pre-patch state."""
|
|
@@ -442,43 +446,58 @@ def ApplyPatch(target, patch_file, exclude=None,
|
|
|
442
446
|
f"({member.file_size} / {member.compress_size}), possible zip bomb."
|
|
443
447
|
)
|
|
444
448
|
|
|
445
|
-
# Extract
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
)
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
449
|
+
# Extract to a temporary file in the same directory, verify its
|
|
450
|
+
# hash, and only then atomically move it into place. A failure
|
|
451
|
+
# at any point (corrupted stream, hash mismatch) can never
|
|
452
|
+
# leave a partially written file at the target path (issue #4).
|
|
453
|
+
fd, tmp_path = tempfile.mkstemp(prefix=".stateman-tmp-", dir=target_file_dir)
|
|
454
|
+
os.close(fd)
|
|
455
|
+
try:
|
|
456
|
+
written = 0
|
|
457
|
+
with patch.open(member) as src, open(tmp_path, "wb") as dst:
|
|
458
|
+
while True:
|
|
459
|
+
chunk = src.read(_CHUNK_SIZE)
|
|
460
|
+
if not chunk:
|
|
461
|
+
break
|
|
462
|
+
written += len(chunk)
|
|
463
|
+
if written > max_member_size:
|
|
464
|
+
raise PatchVerificationError(
|
|
465
|
+
f"Archive member '{filename}' expanded beyond the "
|
|
466
|
+
f"per-member limit of {max_member_size} bytes."
|
|
467
|
+
)
|
|
468
|
+
dst.write(chunk)
|
|
469
|
+
total_extracted += written
|
|
470
|
+
if total_extracted > max_total_size:
|
|
471
|
+
raise PatchVerificationError(
|
|
472
|
+
f"Patch expanded beyond the total size limit of {max_total_size} bytes."
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Verify the hash of the extracted file before moving it into place
|
|
476
|
+
extracted_hash = get_hash(tmp_path, algo)
|
|
477
|
+
expected_hash = expected_hashes.get(filename)
|
|
478
|
+
if not expected_hash:
|
|
479
|
+
raise PatchVerificationError(
|
|
480
|
+
f"No expected hash found in patch metadata for {filename}."
|
|
481
|
+
)
|
|
482
|
+
if extracted_hash != expected_hash:
|
|
483
|
+
raise PatchVerificationError(
|
|
484
|
+
f"Hash mismatch for extracted file {target_path}! "
|
|
485
|
+
f"Expected {expected_hash}, got {extracted_hash}. Patch is corrupted."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
os.chmod(tmp_path, 0o666 & ~umask) # mkstemp creates 0600
|
|
489
|
+
os.replace(tmp_path, target_path) # atomic on POSIX and Windows
|
|
490
|
+
finally:
|
|
491
|
+
# Clean up the temp file if anything failed before the replace
|
|
492
|
+
if os.path.exists(tmp_path):
|
|
493
|
+
try:
|
|
494
|
+
os.remove(tmp_path)
|
|
495
|
+
except OSError:
|
|
496
|
+
pass
|
|
465
497
|
|
|
466
498
|
if filename not in backed_up:
|
|
467
499
|
created.append(filename)
|
|
468
500
|
|
|
469
|
-
# Verify the hash of the extracted file
|
|
470
|
-
extracted_hash = get_hash(target_path, algo)
|
|
471
|
-
expected_hash = expected_hashes.get(filename)
|
|
472
|
-
if not expected_hash:
|
|
473
|
-
raise PatchVerificationError(
|
|
474
|
-
f"No expected hash found in patch metadata for {filename}."
|
|
475
|
-
)
|
|
476
|
-
if extracted_hash != expected_hash:
|
|
477
|
-
raise PatchVerificationError(
|
|
478
|
-
f"Hash mismatch for extracted file {target_path}! "
|
|
479
|
-
f"Expected {expected_hash}, got {extracted_hash}. Patch is corrupted."
|
|
480
|
-
)
|
|
481
|
-
|
|
482
501
|
action = "+" if filename in added else "*"
|
|
483
502
|
print(f"{action} Extracted: {target_path}")
|
|
484
503
|
|
|
File without changes
|