Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
80 lines
3.4 KiB
Python
80 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
"""Read the six inventoried Sargo boot partitions; never open a device for writing."""
|
|
import datetime
|
|
import fcntl
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import stat
|
|
import struct
|
|
import subprocess
|
|
import tempfile
|
|
|
|
PARTITIONS = {
|
|
'boot_a': 67108864, 'boot_b': 67108864,
|
|
'dtbo_a': 8417280, 'dtbo_b': 8417280,
|
|
'vbmeta_a': 65536, 'vbmeta_b': 65536,
|
|
}
|
|
|
|
|
|
def copy_exact(source, destination, size):
|
|
digest = hashlib.sha256()
|
|
remaining = size
|
|
while remaining:
|
|
chunk = source.read(min(1024 * 1024, remaining))
|
|
if not chunk:
|
|
raise RuntimeError('Partition read ended before its declared size')
|
|
destination.write(chunk)
|
|
digest.update(chunk)
|
|
remaining -= len(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def main():
|
|
if os.geteuid() != 0:
|
|
raise RuntimeError('Partition reads require sudo in your own terminal')
|
|
account = pwd.getpwnam('droidian')
|
|
getprop = next((p for p in ('/usr/bin/getprop', '/bin/getprop') if Path(p).exists()), None)
|
|
if not getprop:
|
|
raise RuntimeError('Cannot establish Android device identity')
|
|
device = subprocess.check_output([getprop, 'ro.product.device'], text=True, timeout=10).strip()
|
|
compatible = Path('/proc/device-tree/compatible').read_bytes().split(b'\0')
|
|
if device != 'sargo' or b'google,b4s4-sdm670' not in compatible:
|
|
raise RuntimeError('This recipe only supports the inventoried Sargo adaptation')
|
|
os.umask(0o077)
|
|
folder = Path(tempfile.mkdtemp(prefix='vulpes-boot-backup-', dir='/var/tmp'))
|
|
report = {'recordedAt': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
'device': device, 'rawPartitions': True, 'restorationTested': False,
|
|
'scope': 'Boot, DTBO and vbmeta only; excludes userdata, vendor and system',
|
|
'files': {}}
|
|
for name, expected_size in PARTITIONS.items():
|
|
path = (Path('/dev/disk/by-partlabel') / name).resolve(strict=True)
|
|
fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW)
|
|
with os.fdopen(fd, 'rb') as source:
|
|
if not stat.S_ISBLK(os.fstat(source.fileno()).st_mode):
|
|
raise RuntimeError('Not a block device: ' + str(path))
|
|
size = struct.unpack('Q', fcntl.ioctl(source.fileno(), 0x80081272, bytes(8)))[0]
|
|
if size != expected_size:
|
|
raise RuntimeError('Partition layout differs from the inventory: ' + name)
|
|
target = folder / (name + '.img')
|
|
with target.open('xb') as output:
|
|
digest = copy_exact(source, output, size)
|
|
output.flush()
|
|
os.fsync(output.fileno())
|
|
with target.open('rb') as saved:
|
|
if hashlib.file_digest(saved, 'sha256').hexdigest() != digest:
|
|
raise RuntimeError('Backup verification failed: ' + name)
|
|
report['files'][target.name] = {'source': str(path), 'bytes': size, 'sha256': digest}
|
|
(folder / 'manifest.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
# Keep incomplete backups root-only; transfer ownership only after all six pass.
|
|
for child in folder.iterdir():
|
|
os.chown(child, account.pw_uid, account.pw_gid)
|
|
os.chown(folder, account.pw_uid, account.pw_gid)
|
|
print('Backup verified:', folder)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|