Skip to content

Commit 6841490

Browse files
committed
Move functions under new class
1 parent 747b29b commit 6841490

File tree

6 files changed

+132
-159
lines changed

6 files changed

+132
-159
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,13 @@ You should not use it to repack a custom firmware.
155155

156156
```py
157157
from reolinkfw import ReolinkFirmware, get_info
158-
from reolinkfw.extract import extract_pak
159158

160159
url = "https://reolink-storage.s3.amazonaws.com/website/firmware/20200523firmware/RLC-410-5MP_20_20052300.zip"
161160
print(get_info(url))
162161
file = "/home/ben/RLC-410-5MP_20_20052300.zip"
163162
print(get_info(file))
164163
with ReolinkFirmware.from_file(file) as fw:
165-
extract_pak(fw)
164+
fw.extract_pak()
166165
```
167166

168167
In most cases where a URL is used, it will be a direct link to the file

reolinkfw/__init__.py

Lines changed: 127 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,44 @@
11
import asyncio
2+
import hashlib
23
import io
34
import posixpath
45
import re
5-
from collections.abc import Iterable, Iterator, Mapping
6+
from collections.abc import Iterator, Mapping
7+
from contextlib import redirect_stdout
8+
from functools import partial
69
from pathlib import Path
710
from typing import IO, Any, BinaryIO, Optional, Union
811
from urllib.parse import parse_qsl, urlparse
912
from zipfile import ZipFile, is_zipfile
1013

1114
import aiohttp
15+
import pybcl
1216
from aiohttp.typedefs import StrOrURL
1317
from lxml.etree import fromstring
1418
from lxml.html import document_fromstring
1519
from pakler import PAK, Section, is_pak_file
1620
from pycramfs import Cramfs
21+
from pycramfs.extract import extract_dir as extract_cramfs
1722
from PySquashfsImage import SquashFsImage
23+
from PySquashfsImage.extract import extract_dir as extract_squashfs
24+
from ubireader.ubifs import ubifs as ubifs_
25+
from ubireader.ubifs.output import extract_files as extract_ubifs
26+
from ubireader.ubi_io import ubi_file
27+
from ubireader.utils import guess_leb_size
1828

1929
from reolinkfw.tmpfile import TempFile
2030
from reolinkfw.typedefs import Buffer, Files, StrPath, StrPathURL
2131
from reolinkfw.ubifs import UBIFS
22-
from reolinkfw.uboot import get_arch_name, get_uboot_version, get_uimage_header
32+
from reolinkfw.uboot import LegacyImageHeader, get_arch_name
2333
from reolinkfw.util import (
34+
ONEMIB,
2435
FileType,
2536
SectionFile,
37+
closing_ubifile,
2638
get_cache_file,
2739
get_fs_from_ubi,
2840
has_cache,
2941
make_cache_file,
30-
sha256_pak
3142
)
3243

3344
__version__ = "1.1.0"
@@ -49,6 +60,7 @@ def __init__(self, fd: BinaryIO, offset: int = 0, closefd: bool = True) -> None:
4960
self._kernel_section_name = self._get_kernel_section_name()
5061
self._sdict = {s.name: s for s in self}
5162
self._open_files = 1
63+
self._fs_sections = [s for s in self if s.name in FS_SECTIONS]
5264

5365
def __del__(self) -> None:
5466
self.close()
@@ -93,6 +105,117 @@ def close(self) -> None:
93105
self._fdclose(self._fd)
94106
self._fd = None
95107

108+
def sha256(self) -> str:
109+
sha = hashlib.sha256()
110+
self._fd.seek(0)
111+
for block in iter(partial(self._fd.read, ONEMIB), b''):
112+
sha.update(block)
113+
return sha.hexdigest()
114+
115+
def get_uboot_version(self) -> Optional[str]:
116+
for section in self:
117+
if section.len and "uboot" in section.name.lower():
118+
# This section is always named 'uboot' or 'uboot1'.
119+
with self.open(section) as f:
120+
if f.peek(len(pybcl.BCL_MAGIC_BYTES)) == pybcl.BCL_MAGIC_BYTES:
121+
# Sometimes section.len - sizeof(hdr) is 1 to 3 bytes larger
122+
# than hdr.size. The extra bytes are 0xff (padding?). This
123+
# could explain why the compressed size is added to the header.
124+
hdr = pybcl.HeaderVariant.from_fd(f)
125+
data = pybcl.decompress(f.read(hdr.size), hdr.algo, hdr.outsize)
126+
else:
127+
data = f.read(section.len)
128+
match = re.search(b"U-Boot [0-9]{4}\.[0-9]{2}.*? \(.*?\)", data)
129+
return match.group().decode() if match is not None else None
130+
return None
131+
132+
def get_uimage_header(self) -> LegacyImageHeader:
133+
for section in self:
134+
with self.open(section) as f:
135+
if section.len and FileType.from_magic(f.peek(4)) == FileType.UIMAGE:
136+
# This section is always named 'KERNEL' or 'kernel'.
137+
return LegacyImageHeader.from_fd(f)
138+
raise Exception("No kernel section found")
139+
140+
def get_fs_info(self) -> list[dict[str, str]]:
141+
result = []
142+
for section in self._fs_sections:
143+
with self.open(section) as f:
144+
fs = FileType.from_magic(f.read(4))
145+
if fs == FileType.UBI:
146+
f.seek(266240)
147+
fs = FileType.from_magic(f.read(4))
148+
result.append({
149+
"name": section.name,
150+
"type": fs.name.lower() if fs is not None else "unknown"
151+
})
152+
return result
153+
154+
async def get_info_from_pak(self) -> dict[str, Any]:
155+
ha = await asyncio.to_thread(self.sha256)
156+
app = self._fs_sections[-1]
157+
with self.open(app) as f:
158+
fs = FileType.from_magic(f.read(4))
159+
if fs == FileType.CRAMFS:
160+
files = await asyncio.to_thread(get_files_from_cramfs, f, 0, False)
161+
elif fs == FileType.UBI:
162+
files = await asyncio.to_thread(get_files_from_ubi, f, app.len, 0)
163+
elif fs == FileType.SQUASHFS:
164+
files = await asyncio.to_thread(get_files_from_squashfs, f, 0, False)
165+
else:
166+
return {"error": "Unrecognized image type", "sha256": ha}
167+
uimage = self.get_uimage_header()
168+
return {
169+
**get_info_from_files(files),
170+
"os": "Linux" if uimage.os == 5 else "Unknown",
171+
"architecture": get_arch_name(uimage.arch),
172+
"kernel_image_name": uimage.name,
173+
"uboot_version": self.get_uboot_version(),
174+
"filesystems": self.get_fs_info(),
175+
"sha256": ha
176+
}
177+
178+
def extract_file_system(self, section: Section, dest: Optional[Path] = None) -> None:
179+
dest = (Path.cwd() / "reolink_fs") if dest is None else dest
180+
dest.mkdir(parents=True, exist_ok=True)
181+
with self.open(section) as f:
182+
fs = FileType.from_magic(f.read(4))
183+
if fs == FileType.UBI:
184+
fs_bytes = get_fs_from_ubi(f, section.len, 0)
185+
fs = FileType.from_magic(fs_bytes[:4])
186+
if fs == FileType.UBIFS:
187+
with TempFile(fs_bytes) as file:
188+
block_size = guess_leb_size(file)
189+
with closing_ubifile(ubi_file(file, block_size)) as ubifile:
190+
with redirect_stdout(io.StringIO()):
191+
# Files that already exist are not written again.
192+
extract_ubifs(ubifs_(ubifile), dest)
193+
elif fs == FileType.SQUASHFS:
194+
with SquashFsImage.from_bytes(fs_bytes) as image:
195+
extract_squashfs(image.root, dest, True)
196+
else:
197+
raise Exception("Unknown file system in UBI")
198+
elif fs == FileType.SQUASHFS:
199+
with SquashFsImage(f, 0, False) as image:
200+
extract_squashfs(image.root, dest, True)
201+
elif fs == FileType.CRAMFS:
202+
with Cramfs.from_fd(f, 0, False) as image:
203+
extract_cramfs(image.rootdir, dest, True)
204+
else:
205+
raise Exception("Unknown file system")
206+
207+
def extract_pak(self, dest: Optional[Path] = None, force: bool = False) -> None:
208+
dest = (Path.cwd() / "reolink_firmware") if dest is None else dest
209+
dest.mkdir(parents=True, exist_ok=force)
210+
rootfsdir = [s.name for s in self if s.name in ROOTFS_SECTIONS][0]
211+
for section in self:
212+
if section.name in FS_SECTIONS:
213+
if section.name == "app":
214+
outpath = dest / rootfsdir / "mnt" / "app"
215+
else:
216+
outpath = dest / rootfsdir
217+
self.extract_file_system(section, outpath)
218+
96219

97220
async def download(url: StrOrURL) -> Union[bytes, int]:
98221
"""Return resource as bytes.
@@ -187,47 +310,6 @@ def is_local_file(string: StrPath) -> bool:
187310
return Path(string).is_file()
188311

189312

190-
def get_fs_info(fw: ReolinkFirmware, fs_sections: Iterable[Section]) -> list[dict[str, str]]:
191-
result = []
192-
for section in fs_sections:
193-
with fw.open(section) as f:
194-
fs = FileType.from_magic(f.read(4))
195-
if fs == FileType.UBI:
196-
f.seek(266240)
197-
fs = FileType.from_magic(f.read(4))
198-
result.append({
199-
"name": section.name,
200-
"type": fs.name.lower() if fs is not None else "unknown"
201-
})
202-
return result
203-
204-
205-
async def get_info_from_pak(fw: ReolinkFirmware) -> dict[str, Any]:
206-
ha = await asyncio.to_thread(sha256_pak, fw)
207-
fs_sections = [s for s in fw if s.name in FS_SECTIONS]
208-
app = fs_sections[-1]
209-
with fw.open(app) as f:
210-
fs = FileType.from_magic(f.read(4))
211-
if fs == FileType.CRAMFS:
212-
files = await asyncio.to_thread(get_files_from_cramfs, f, 0, False)
213-
elif fs == FileType.UBI:
214-
files = await asyncio.to_thread(get_files_from_ubi, f, app.len, 0)
215-
elif fs == FileType.SQUASHFS:
216-
files = await asyncio.to_thread(get_files_from_squashfs, f, 0, False)
217-
else:
218-
return {"error": "Unrecognized image type", "sha256": ha}
219-
uimage = get_uimage_header(fw)
220-
return {
221-
**get_info_from_files(files),
222-
"os": "Linux" if uimage.os == 5 else "Unknown",
223-
"architecture": get_arch_name(uimage.arch),
224-
"kernel_image_name": uimage.name,
225-
"uboot_version": get_uboot_version(fw),
226-
"filesystems": get_fs_info(fw, fs_sections),
227-
"sha256": ha
228-
}
229-
230-
231313
async def direct_download_url(url: str) -> str:
232314
if url.startswith("https://drive.google.com/file/d/"):
233315
return f"https://drive.google.com/uc?id={url.split('/')[5]}&confirm=t"
@@ -290,7 +372,7 @@ async def get_info(file_or_url: StrPathURL, use_cache: bool = True) -> list[dict
290372
return [{"file": file_or_url, "error": str(e)}]
291373
if not paks:
292374
return [{"file": file_or_url, "error": "No PAKs found in ZIP file"}]
293-
info = [{**await get_info_from_pak(pakfile), "file": file_or_url, "pak": pakname} for pakname, pakfile in paks]
375+
info = [{**await pakfile.get_info_from_pak(), "file": file_or_url, "pak": pakname} for pakname, pakfile in paks]
294376
for _, pakfile in paks:
295377
pakfile.close()
296378
return info

reolinkfw/__main__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
from pathlib import Path, PurePath
99

1010
from reolinkfw import __version__, get_info, get_paks
11-
from reolinkfw.extract import extract_pak
12-
from reolinkfw.util import sha256_pak
1311

1412
HW_FIELDS = ("board_type", "detail_machine_type", "board_name")
1513

@@ -50,8 +48,8 @@ async def extract(args: Namespace) -> None:
5048
raise Exception("No PAKs found in ZIP file")
5149
dest = Path.cwd() if args.dest is None else args.dest
5250
for pakname, pakfile in paks:
53-
name = sha256_pak(pakfile) if pakname is None else PurePath(pakname).stem
54-
await asyncio.to_thread(extract_pak, pakfile, dest / name, args.force)
51+
name = pakfile.sha256() if pakname is None else PurePath(pakname).stem
52+
await asyncio.to_thread(pakfile.extract_pak, dest / name, args.force)
5553
pakfile.close()
5654

5755

reolinkfw/extract.py

Lines changed: 0 additions & 61 deletions
This file was deleted.

reolinkfw/uboot.py

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
from __future__ import annotations
22

3-
import re
43
from ctypes import BigEndianStructure, c_char, c_uint32, c_uint8, sizeof
54
from enum import IntEnum
6-
from typing import TYPE_CHECKING, BinaryIO, Optional
7-
8-
import pybcl
9-
10-
if TYPE_CHECKING:
11-
from reolinkfw import ReolinkFirmware
12-
from reolinkfw.util import FileType
5+
from typing import BinaryIO
136

147
UBOOT_MAGIC = 0x27051956
158

@@ -102,33 +95,6 @@ def from_fd(cls, fd: BinaryIO) -> LegacyImageHeader:
10295
return cls.from_buffer_copy(fd.read(sizeof(cls)))
10396

10497

105-
def get_uboot_version(fw: ReolinkFirmware) -> Optional[str]:
106-
for section in fw:
107-
if section.len and "uboot" in section.name.lower():
108-
# This section is always named 'uboot' or 'uboot1'.
109-
with fw.open(section) as f:
110-
if f.peek(len(pybcl.BCL_MAGIC_BYTES)) == pybcl.BCL_MAGIC_BYTES:
111-
# Sometimes section.len - sizeof(hdr) is 1 to 3 bytes larger
112-
# than hdr.size. The extra bytes are 0xff (padding?). This
113-
# could explain why the compressed size is added to the header.
114-
hdr = pybcl.HeaderVariant.from_fd(f)
115-
data = pybcl.decompress(f.read(hdr.size), hdr.algo, hdr.outsize)
116-
else:
117-
data = f.read(section.len)
118-
match = re.search(b"U-Boot [0-9]{4}\.[0-9]{2}.*? \(.*?\)", data)
119-
return match.group().decode() if match is not None else None
120-
return None
121-
122-
123-
def get_uimage_header(fw: ReolinkFirmware) -> LegacyImageHeader:
124-
for section in fw:
125-
with fw.open(section) as f:
126-
if section.len and FileType.from_magic(f.peek(4)) == FileType.UIMAGE:
127-
# This section is always named 'KERNEL' or 'kernel'.
128-
return LegacyImageHeader.from_fd(f)
129-
raise Exception("No kernel section found")
130-
131-
13298
def get_arch_name(arch: Arch) -> str:
13399
if arch == Arch.ARM:
134100
return "ARM"

0 commit comments

Comments
 (0)