mirror of
https://github.com/leminlimez/Nugget.git
synced 2025-04-08 04:23:05 +08:00
221 lines
7.7 KiB
Python
221 lines
7.7 KiB
Python
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
import plistlib
|
|
from pathlib import Path
|
|
from base64 import b64decode
|
|
from hashlib import sha1
|
|
from . import mbdb
|
|
from .mbdb import _FileMode
|
|
from random import randbytes
|
|
from typing import Optional
|
|
|
|
# RWX:RX:RX
|
|
DEFAULT = _FileMode.S_IRUSR | _FileMode.S_IWUSR | _FileMode.S_IXUSR | _FileMode.S_IRGRP | _FileMode.S_IXGRP | _FileMode.S_IROTH | _FileMode.S_IXOTH
|
|
|
|
@dataclass
|
|
class BackupFile:
|
|
path: str
|
|
domain: str
|
|
|
|
def to_record(self) -> mbdb.MbdbRecord:
|
|
raise NotImplementedError()
|
|
|
|
@dataclass
|
|
class ConcreteFile(BackupFile):
|
|
contents: bytes
|
|
src_path: Optional[str] = None
|
|
owner: int = 0
|
|
group: int = 0
|
|
inode: Optional[int] = None
|
|
mode: _FileMode = DEFAULT
|
|
|
|
hash: bytes = None
|
|
size: int = None
|
|
|
|
def read_contents(self) -> bytes:
|
|
contents = self.contents
|
|
if self.contents == None:
|
|
with open(self.src_path, "rb") as in_file:
|
|
contents = in_file.read()
|
|
# prepopulate hash and size
|
|
self.hash = sha1(contents).digest()
|
|
self.size = len(contents)
|
|
return contents
|
|
|
|
def to_record(self) -> mbdb.MbdbRecord:
|
|
if self.inode is None:
|
|
self.inode = int.from_bytes(randbytes(8), "big")
|
|
if self.hash == None or self.size == None:
|
|
self.read_contents()
|
|
return mbdb.MbdbRecord(
|
|
domain=self.domain,
|
|
filename=self.path,
|
|
link="",
|
|
hash=self.hash,
|
|
key=b"",
|
|
mode=self.mode | _FileMode.S_IFREG,
|
|
#unknown2=0,
|
|
#unknown3=0,
|
|
inode=self.inode,
|
|
user_id=self.owner,
|
|
group_id=self.group,
|
|
mtime=int(datetime.now().timestamp()),
|
|
atime=int(datetime.now().timestamp()),
|
|
ctime=int(datetime.now().timestamp()),
|
|
size=self.size,
|
|
flags=4,
|
|
properties=[]
|
|
)
|
|
|
|
@dataclass
|
|
class Directory(BackupFile):
|
|
owner: int = 0
|
|
group: int = 0
|
|
mode: _FileMode = DEFAULT
|
|
|
|
def to_record(self) -> mbdb.MbdbRecord:
|
|
return mbdb.MbdbRecord(
|
|
domain=self.domain,
|
|
filename=self.path,
|
|
link="",
|
|
hash=b"",
|
|
key=b"",
|
|
mode=self.mode | _FileMode.S_IFDIR,
|
|
#unknown2=0,
|
|
#unknown3=0,
|
|
inode=0, # inode is not respected for directories
|
|
user_id=self.owner,
|
|
group_id=self.group,
|
|
mtime=int(datetime.now().timestamp()),
|
|
atime=int(datetime.now().timestamp()),
|
|
ctime=int(datetime.now().timestamp()),
|
|
size=0,
|
|
flags=4,
|
|
properties=[]
|
|
)
|
|
|
|
@dataclass
|
|
class SymbolicLink(BackupFile):
|
|
target: str
|
|
owner: int = 0
|
|
group: int = 0
|
|
inode: Optional[int] = None
|
|
mode: _FileMode = DEFAULT
|
|
|
|
def to_record(self) -> mbdb.MbdbRecord:
|
|
if self.inode is None:
|
|
self.inode = int.from_bytes(randbytes(8), "big")
|
|
return mbdb.MbdbRecord(
|
|
domain=self.domain,
|
|
filename=self.path,
|
|
link=self.target,
|
|
hash=b"",
|
|
key=b"",
|
|
mode=self.mode | _FileMode.S_IFLNK,
|
|
#unknown2=0,
|
|
#unknown3=0,
|
|
inode=self.inode,
|
|
user_id=self.owner,
|
|
group_id=self.group,
|
|
mtime=int(datetime.now().timestamp()),
|
|
atime=int(datetime.now().timestamp()),
|
|
ctime=int(datetime.now().timestamp()),
|
|
size=0,
|
|
flags=4,
|
|
properties=[]
|
|
)
|
|
|
|
@dataclass
|
|
class AppBundle:
|
|
identifier: str
|
|
path: str
|
|
container_content_class: str
|
|
version: str = 804
|
|
|
|
@dataclass
|
|
class Backup:
|
|
files: list[BackupFile]
|
|
apps: list[AppBundle]
|
|
|
|
def write_to_directory(self, directory: Path):
|
|
for file in self.files:
|
|
if isinstance(file, ConcreteFile):
|
|
#print("Writing", file.path, "to", directory / sha1((file.domain + "-" + file.path).encode()).digest().hex())
|
|
with open(directory / sha1((file.domain + "-" + file.path).encode()).digest().hex(), "wb") as f:
|
|
f.write(file.read_contents())
|
|
|
|
with open(directory / "Manifest.mbdb", "wb") as f:
|
|
f.write(self.generate_manifest_db().to_bytes())
|
|
|
|
with open(directory / "Status.plist", "wb") as f:
|
|
f.write(self.generate_status())
|
|
|
|
with open(directory / "Manifest.plist", "wb") as f:
|
|
f.write(self.generate_manifest())
|
|
|
|
with open(directory / "Info.plist", "wb") as f:
|
|
f.write(plistlib.dumps({}))
|
|
|
|
|
|
def generate_manifest_db(self): # Manifest.mbdb
|
|
records = []
|
|
for file in self.files:
|
|
records.append(file.to_record())
|
|
return mbdb.Mbdb(records=records)
|
|
|
|
def generate_status(self) -> bytes: # Status.plist
|
|
return plistlib.dumps({
|
|
"BackupState": "new",
|
|
"Date": datetime.fromisoformat("1970-01-01T00:00:00+00:00"),
|
|
"IsFullBackup": False,
|
|
"SnapshotState": "finished",
|
|
"UUID": "00000000-0000-0000-0000-000000000000",
|
|
"Version": "2.4"
|
|
})
|
|
|
|
def generate_manifest(self) -> bytes: # Manifest.plist
|
|
plist = {
|
|
"BackupKeyBag": b64decode("""
|
|
VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABDud41d1b9NBICR1BH9JfVtSE1D
|
|
SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA
|
|
AAQAAAAAU0FMVAAAABRY5Ne2bthGQ5rf4O3gikep1e6tZUlURVIAAAAEAAAnEFVVSUQA
|
|
AAAQB7R8awiGR9aba1UuVahGPENMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAE
|
|
AAAAAFdQS1kAAAAoN3kQAJloFg+ukEUY+v5P+dhc/Welw/oucsyS40UBh67ZHef5ZMk9
|
|
UVVVSUQAAAAQgd0cg0hSTgaxR3PVUbcEkUNMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktU
|
|
WVAAAAAEAAAAAFdQS1kAAAAoMiQTXx0SJlyrGJzdKZQ+SfL124w+2Tf/3d1R2i9yNj9z
|
|
ZCHNJhnorVVVSUQAAAAQf7JFQiBOS12JDD7qwKNTSkNMQVMAAAAEAAAAA1dSQVAAAAAE
|
|
AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoSEelorROJA46ZUdwDHhMKiRguQyqHukotrxh
|
|
jIfqiZ5ESBXX9txi51VVSUQAAAAQfF0G/837QLq01xH9+66vx0NMQVMAAAAEAAAABFdS
|
|
QVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAol0BvFhd5bu4Hr75XqzNf4g0fMqZA
|
|
ie6OxI+x/pgm6Y95XW17N+ZIDVVVSUQAAAAQimkT2dp1QeadMu1KhJKNTUNMQVMAAAAE
|
|
AAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo2N2DZarQ6GPoWRgTiy/t
|
|
djKArOqTaH0tPSG9KLbIjGTOcLodhx23xFVVSUQAAAAQQV37JVZHQFiKpoNiGmT6+ENM
|
|
QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAofe2QSvDC2cV7
|
|
Etk4fSBbgqDx5ne/z1VHwmJ6NdVrTyWi80Sy869DM1VVSUQAAAAQFzkdH+VgSOmTj3yE
|
|
cfWmMUNMQVMAAAAEAAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo7kLY
|
|
PQ/DnHBERGpaz37eyntIX/XzovsS0mpHW3SoHvrb9RBgOB+WblVVSUQAAAAQEBpgKOz9
|
|
Tni8F9kmSXd0sENMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kA
|
|
AAAo5mxVoyNFgPMzphYhm1VG8Fhsin/xX+r6mCd9gByF5SxeolAIT/ICF1VVSUQAAAAQ
|
|
rfKB2uPSQtWh82yx6w4BoUNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAA
|
|
AFdQS1kAAAAo5iayZBwcRa1c1MMx7vh6lOYux3oDI/bdxFCW1WHCQR/Ub1MOv+QaYFVV
|
|
SUQAAAAQiLXvK3qvQza/mea5inss/0NMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tUWVAA
|
|
AAAEAAAAAFdQS1kAAAAoD2wHX7KriEe1E31z7SQ7/+AVymcpARMYnQgegtZD0Mq2U55u
|
|
xwNr2FVVSUQAAAAQ/Q9feZxLS++qSe/a4emRRENMQVMAAAAEAAAAC1dSQVAAAAAEAAAA
|
|
A0tUWVAAAAAEAAAAAFdQS1kAAAAocYda2jyYzzSKggRPw/qgh6QPESlkZedgDUKpTr4Z
|
|
Z8FDgd7YoALY1g=="""),
|
|
"Lockdown": {},
|
|
"SystemDomainsVersion": "20.0",
|
|
"Version": "9.1"
|
|
}
|
|
# add the apps
|
|
if len(self.apps) > 0:
|
|
plist["Applications"] = {}
|
|
for app in self.apps:
|
|
appInfo = {
|
|
"CFBundleIdentifier": app.identifier,
|
|
"CFBundleVersion": app.version,
|
|
"ContainerContentClass": app.container_content_class,
|
|
"Path": app.path
|
|
}
|
|
plist["Applications"][app.identifier] = appInfo
|
|
return plistlib.dumps(plist) |