11import asyncio
2+ import hashlib
23import io
34import posixpath
45import 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
69from pathlib import Path
710from typing import IO , Any , BinaryIO , Optional , Union
811from urllib .parse import parse_qsl , urlparse
912from zipfile import ZipFile , is_zipfile
1013
1114import aiohttp
15+ import pybcl
1216from aiohttp .typedefs import StrOrURL
1317from lxml .etree import fromstring
1418from lxml .html import document_fromstring
1519from pakler import PAK , Section , is_pak_file
1620from pycramfs import Cramfs
21+ from pycramfs .extract import extract_dir as extract_cramfs
1722from 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
1929from reolinkfw .tmpfile import TempFile
2030from reolinkfw .typedefs import Buffer , Files , StrPath , StrPathURL
2131from 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
2333from 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
97220async 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-
231313async 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
0 commit comments