import os
import sys
import hashlib
import base64
from pathlib import Path
def b32_hash(content: str) -> str:
"""
Computes a base32-encoded hash of the input string using SHA256 and XOR folding.
This function performs the following steps:
1. Compute hash: SHA256 hash of the input string. (Output: 32-byte bytes object)
2. Compress hash: XOR folding to reduce to 20 bytes. (Output: 20-byte bytearray)
3. Encode base32: Base32 encoding of the compressed hash. (Output: 32-character string)
4. Convert lowercase: Lowercase conversion of the base32 string. (Output: 32-character lowercase string)
Args:
content (str): The input string to hash.
Returns:
str: A base32-encoded, lowercase string representing the compressed hash.
"""
sha256_hash = hashlib.sha256(content.encode()).digest()
compressed_hash = bytearray(20)
for i in range(20):
compressed_hash[i] = sha256_hash[i] ^ sha256_hash[i + 12]
b32sum = base64.b32encode(compressed_hash).decode()
return b32sum.lower()
def epkg_store_hash(epkg_path: str) -> str:
"""Compute the hash of the contents of a directory."""
dir_path = Path(epkg_path)
paths = []
dir_path = Path(dir_path)
fs_path = str(dir_path / "fs")
install_path = str(dir_path / "info" / "install")
for root, dirs, files in os.walk(dir_path):
for name in files + dirs:
path = str(Path(root) / name)
if (path.startswith(fs_path) or path.startswith(install_path)):
paths.append(Path(path))
paths.sort()
info = []
for path in paths:
ftype, fdata = get_path_info(path)
info.append(str(path.relative_to(dir_path)))
info.append(ftype)
info.append(fdata)
all_info = "\n".join(info)
return b32_hash(all_info)
def get_path_info(path: Path):
"""Get the type, size, and content hash of a file or directory."""
stat = os.lstat(path)
if stat.st_mode & 0o170000 == 0o120000:
ftype = "S_IFLNK"
fdata = os.readlink(path)
elif stat.st_mode & 0o170000 == 0o100000:
ftype = "S_IFREG"
fdata = " ".join(file_sha256_chunks(path, stat))
elif stat.st_mode & 0o170000 == 0o060000:
ftype = "S_IFBLK"
fdata = str(stat.st_dev)
elif stat.st_mode & 0o170000 == 0o020000:
ftype = "S_IFCHR"
fdata = str(stat.st_dev)
elif stat.st_mode & 0o170000 == 0o040000:
ftype = "S_IFDIR"
fdata = ""
elif stat.st_mode & 0o170000 == 0o010000:
ftype = "S_IFIFO"
fdata = ""
elif stat.st_mode & 0o170000 == 0o140000:
ftype = "S_IFSOCK"
fdata = ""
else:
raise ValueError(f"Encountered an unknown file type at: {path}")
return ftype, fdata
def file_sha256_chunks(file_path: Path, stat) -> list[str]:
"""Compute the SHA-256 hash for every 16 KB chunk of a file."""
CHUNK_SIZE = 16 << 10
hashes = [str(stat.st_size)]
with open(file_path, "rb") as file:
while chunk := file.read(CHUNK_SIZE):
sha256 = hashlib.sha256(chunk).hexdigest()
hashes.append(sha256)
return hashes
if __name__ == "__main__":
for epkg_path in sys.argv[1:]:
hash_result = epkg_store_hash(epkg_path)
print(f"{hash_result}")