remarkable-update-image 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- remarkable_update_image-1.0.0/LICENSE +21 -0
- remarkable_update_image-1.0.0/PKG-INFO +73 -0
- remarkable_update_image-1.0.0/README.md +47 -0
- remarkable_update_image-1.0.0/pyproject.toml +42 -0
- remarkable_update_image-1.0.0/remarkable_update_image/__init__.py +3 -0
- remarkable_update_image-1.0.0/remarkable_update_image/image.py +276 -0
- remarkable_update_image-1.0.0/remarkable_update_image/update_metadata_pb2.py +42 -0
- remarkable_update_image-1.0.0/remarkable_update_image.egg-info/PKG-INFO +73 -0
- remarkable_update_image-1.0.0/remarkable_update_image.egg-info/SOURCES.txt +12 -0
- remarkable_update_image-1.0.0/remarkable_update_image.egg-info/dependency_links.txt +1 -0
- remarkable_update_image-1.0.0/remarkable_update_image.egg-info/requires.txt +3 -0
- remarkable_update_image-1.0.0/remarkable_update_image.egg-info/top_level.txt +1 -0
- remarkable_update_image-1.0.0/requirements.txt +3 -0
- remarkable_update_image-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nathaniel van Diepen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: remarkable_update_image
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Read reMarkable update images
|
|
5
|
+
Author-email: Eeems <eeems@eeems.email>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Eeems/remarkable-update-image
|
|
7
|
+
Project-URL: Repository, https://github.com/Eeems/remarkable-update-image.git
|
|
8
|
+
Project-URL: Issues, https://github.com/Eeems/remarkable-update-image/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: cryptography==41.0.7
|
|
24
|
+
Requires-Dist: protobuf==4.21.1
|
|
25
|
+
Requires-Dist: ext4==1.0.2
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/remarkable_update_image)
|
|
28
|
+
|
|
29
|
+
# reMarkable Update Image
|
|
30
|
+
Read a reMarkable update image as a block device.
|
|
31
|
+
|
|
32
|
+
## Known Issues
|
|
33
|
+
|
|
34
|
+
- Will report checksum errors for Directory inode, even though they are fine
|
|
35
|
+
- Will report checksum errors for extent headers, even though they are fine
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from ext4 import Volume
|
|
41
|
+
from remarkable_update_image import UpdateImage
|
|
42
|
+
|
|
43
|
+
image = UpdateImage("path/to/update/file.signed")
|
|
44
|
+
|
|
45
|
+
# Extract raw ext4 image
|
|
46
|
+
with open("image.ext4", "wb") as f:
|
|
47
|
+
f.write(image.read())
|
|
48
|
+
|
|
49
|
+
# Extract specific file
|
|
50
|
+
volume = Volume(image)
|
|
51
|
+
inode = volume.inode_at("/etc/version")
|
|
52
|
+
with open("version", "wb") as f:
|
|
53
|
+
f.write(inode.open().read())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Building
|
|
57
|
+
Dependencies:
|
|
58
|
+
- curl
|
|
59
|
+
- protoc
|
|
60
|
+
- python
|
|
61
|
+
- python-build
|
|
62
|
+
- python-pip
|
|
63
|
+
- python-pipx
|
|
64
|
+
- python-venv
|
|
65
|
+
- python-wheel
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
make # Build wheel and sdist packages in dist/
|
|
69
|
+
make wheel # Build wheel package in dist/
|
|
70
|
+
make sdist # Build sdist package in dist/
|
|
71
|
+
make test # Run unit tests
|
|
72
|
+
make install # Build wheel and install it with pipx or pip install --user
|
|
73
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[](https://pypi.org/project/remarkable_update_image)
|
|
2
|
+
|
|
3
|
+
# reMarkable Update Image
|
|
4
|
+
Read a reMarkable update image as a block device.
|
|
5
|
+
|
|
6
|
+
## Known Issues
|
|
7
|
+
|
|
8
|
+
- Will report checksum errors for Directory inode, even though they are fine
|
|
9
|
+
- Will report checksum errors for extent headers, even though they are fine
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from ext4 import Volume
|
|
15
|
+
from remarkable_update_image import UpdateImage
|
|
16
|
+
|
|
17
|
+
image = UpdateImage("path/to/update/file.signed")
|
|
18
|
+
|
|
19
|
+
# Extract raw ext4 image
|
|
20
|
+
with open("image.ext4", "wb") as f:
|
|
21
|
+
f.write(image.read())
|
|
22
|
+
|
|
23
|
+
# Extract specific file
|
|
24
|
+
volume = Volume(image)
|
|
25
|
+
inode = volume.inode_at("/etc/version")
|
|
26
|
+
with open("version", "wb") as f:
|
|
27
|
+
f.write(inode.open().read())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Building
|
|
31
|
+
Dependencies:
|
|
32
|
+
- curl
|
|
33
|
+
- protoc
|
|
34
|
+
- python
|
|
35
|
+
- python-build
|
|
36
|
+
- python-pip
|
|
37
|
+
- python-pipx
|
|
38
|
+
- python-venv
|
|
39
|
+
- python-wheel
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
make # Build wheel and sdist packages in dist/
|
|
43
|
+
make wheel # Build wheel package in dist/
|
|
44
|
+
make sdist # Build sdist package in dist/
|
|
45
|
+
make test # Run unit tests
|
|
46
|
+
make install # Build wheel and install it with pipx or pip install --user
|
|
47
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "remarkable_update_image"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Eeems", email="eeems@eeems.email" },
|
|
6
|
+
]
|
|
7
|
+
description = "Read reMarkable update images"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Topic :: System :: Filesystems",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
dynamic = ["dependencies", "readme"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/Eeems/remarkable-update-image"
|
|
26
|
+
Repository = "https://github.com/Eeems/remarkable-update-image.git"
|
|
27
|
+
Issues = "https://github.com/Eeems/remarkable-update-image/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = [
|
|
31
|
+
"remarkable_update_image",
|
|
32
|
+
]
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
remarkable_update_image = ['update_metadata.proto']
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.dynamic]
|
|
37
|
+
dependencies = {file = ["requirements.txt"]}
|
|
38
|
+
readme = {file= ["README.md"], content-type = "text/markdown"}
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["setuptools>=42", "wheel", "nuitka", "toml"]
|
|
42
|
+
build-backend = "nuitka.distutils.Build"
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import bz2
|
|
2
|
+
import io
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from cachetools import TTLCache
|
|
9
|
+
from hashlib import sha256
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
11
|
+
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
|
13
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
14
|
+
from .update_metadata_pb2 import DeltaArchiveManifest
|
|
15
|
+
from .update_metadata_pb2 import InstallOperation
|
|
16
|
+
from .update_metadata_pb2 import Signatures
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sizeof_fmt(num, suffix="B"):
|
|
20
|
+
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
|
|
21
|
+
if abs(num) < 1024.0:
|
|
22
|
+
return f"{num:3.1f}{unit}{suffix}"
|
|
23
|
+
num /= 1024.0
|
|
24
|
+
return f"{num:.1f}Yi{suffix}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def range_contains(range1, range2):
|
|
28
|
+
return range1.start < range2.stop and range2.start < range1.stop
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BlockCache(TTLCache):
|
|
32
|
+
def __init__(self, maxsize, ttl, timer=time.monotonic, getsizeof=sys.getsizeof):
|
|
33
|
+
super().__init__(maxsize, ttl, timer, getsizeof)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def usage_str(self):
|
|
37
|
+
return f"{self.curr_size_str}/{self.max_size_str}"
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def curr_size_str(self):
|
|
41
|
+
return sizeof_fmt(self.currsize)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def max_size_str(self):
|
|
45
|
+
return sizeof_fmt(self.maxsize)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UpdateImageException(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UpdateImageSignatureException(UpdateImageException):
|
|
53
|
+
def __init__(self, message, signed_hash, actual_hash):
|
|
54
|
+
super().__init__(self, message)
|
|
55
|
+
self.signed_hash = signed_hash
|
|
56
|
+
self.actual_hash = actual_hash
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class UpdateImage(io.RawIOBase):
|
|
60
|
+
_manifest = None
|
|
61
|
+
_offset = -1
|
|
62
|
+
_size = 0
|
|
63
|
+
_pos = 0
|
|
64
|
+
|
|
65
|
+
def __init__(self, update_file, cache_size=500, cache_ttl=60):
|
|
66
|
+
self.update_file = update_file
|
|
67
|
+
self.cache_size = cache_size
|
|
68
|
+
self._cache = BlockCache(
|
|
69
|
+
maxsize=cache_size * 1024 * 1024,
|
|
70
|
+
ttl=cache_ttl,
|
|
71
|
+
)
|
|
72
|
+
with open(self.update_file, "rb") as f:
|
|
73
|
+
magic = f.read(4)
|
|
74
|
+
if magic != b"CrAU":
|
|
75
|
+
raise UpdateImageException("Wrong header")
|
|
76
|
+
|
|
77
|
+
major = struct.unpack(">Q", f.read(8))[0]
|
|
78
|
+
if major != 1:
|
|
79
|
+
raise UpdateImageException("Unsupported version")
|
|
80
|
+
|
|
81
|
+
size = struct.unpack(">Q", f.read(8))[0]
|
|
82
|
+
data = f.read(size)
|
|
83
|
+
self._manifest = DeltaArchiveManifest.FromString(data)
|
|
84
|
+
self._offset = f.tell()
|
|
85
|
+
|
|
86
|
+
for blob, offset, length, f in self._blobs:
|
|
87
|
+
self._size += length
|
|
88
|
+
|
|
89
|
+
def verify(self, publickey):
|
|
90
|
+
_publickey = load_pem_public_key(publickey)
|
|
91
|
+
with open(self.update_file, "rb") as f:
|
|
92
|
+
data = f.read(self._offset + self._manifest.signatures_offset)
|
|
93
|
+
|
|
94
|
+
actual_hash = sha256(data).digest()
|
|
95
|
+
signed_hash = _publickey.recover_data_from_signature(
|
|
96
|
+
self.signature,
|
|
97
|
+
PKCS1v15(),
|
|
98
|
+
SHA256,
|
|
99
|
+
)
|
|
100
|
+
if actual_hash != signed_hash:
|
|
101
|
+
raise UpdateImageSignatureException(
|
|
102
|
+
"Actual hash does not match signed hash", signed_hash, actual_hash
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def block_size(self):
|
|
107
|
+
return self._manifest.block_size
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def signature(self):
|
|
111
|
+
for signature in self._signatures:
|
|
112
|
+
if signature.version == 2:
|
|
113
|
+
return signature.data
|
|
114
|
+
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def _signatures(self):
|
|
119
|
+
with open(self.update_file, "rb") as f:
|
|
120
|
+
f.seek(self._offset + self._manifest.signatures_offset)
|
|
121
|
+
for signature in Signatures.FromString(
|
|
122
|
+
f.read(self._manifest.signatures_size)
|
|
123
|
+
).signatures:
|
|
124
|
+
yield signature
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def _blobs(self):
|
|
128
|
+
with open(self.update_file, "rb") as f:
|
|
129
|
+
for blob in self._manifest.partition_operations:
|
|
130
|
+
f.seek(self._offset + blob.data_offset)
|
|
131
|
+
dst_offset = blob.dst_extents[0].start_block * self.block_size
|
|
132
|
+
dst_length = blob.dst_extents[0].num_blocks * self.block_size
|
|
133
|
+
if blob.type not in (0, 1):
|
|
134
|
+
raise UpdateImageException(f"Unsupported type {blob.type}")
|
|
135
|
+
|
|
136
|
+
yield blob, dst_offset, dst_length, f
|
|
137
|
+
|
|
138
|
+
self.expire()
|
|
139
|
+
|
|
140
|
+
def _read_blob(self, blob, blob_offset, blob_length, f):
|
|
141
|
+
if blob_offset in self._cache:
|
|
142
|
+
return self._cache[blob_offset]
|
|
143
|
+
|
|
144
|
+
if blob.type not in (
|
|
145
|
+
InstallOperation.Type.REPLACE,
|
|
146
|
+
InstallOperation.Type.REPLACE_BZ,
|
|
147
|
+
):
|
|
148
|
+
raise NotImplementedError(
|
|
149
|
+
f"Error: {InstallOperation.Type.keys()[blob.type]} has not been implemented yet"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
blob_data = f.read(blob.data_length)
|
|
153
|
+
if sha256(blob_data).digest() != blob.data_sha256_hash:
|
|
154
|
+
raise UpdateImageException("Error: Data has wrong sha256sum")
|
|
155
|
+
|
|
156
|
+
if blob.type == InstallOperation.Type.REPLACE_BZ:
|
|
157
|
+
try:
|
|
158
|
+
blob_data = bz2.decompress(blob_data)
|
|
159
|
+
|
|
160
|
+
except ValueError as err:
|
|
161
|
+
raise UpdateImageException(f"Error: {err}") from err
|
|
162
|
+
|
|
163
|
+
if blob_length - len(blob_data) < 0:
|
|
164
|
+
raise UpdateImageException(
|
|
165
|
+
f"Error: Bz2 compressed data was the wrong length {len(blob_data)}"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
self._cache[blob_offset] = blob_data
|
|
170
|
+
except ValueError as err:
|
|
171
|
+
if str(err) != "value too large":
|
|
172
|
+
raise err
|
|
173
|
+
|
|
174
|
+
return blob_data
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def cache(self):
|
|
178
|
+
return self._cache
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def size(self):
|
|
182
|
+
return self._size
|
|
183
|
+
|
|
184
|
+
def expire(self):
|
|
185
|
+
self._cache.expire()
|
|
186
|
+
|
|
187
|
+
def writable(self):
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def seekable(self):
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def readable(self):
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def seek(self, offset, whence=os.SEEK_SET):
|
|
197
|
+
if whence not in (os.SEEK_SET, os.SEEK_CUR, os.SEEK_END):
|
|
198
|
+
raise OSError("Not supported whence")
|
|
199
|
+
if whence == os.SEEK_SET and offset < 0:
|
|
200
|
+
raise ValueError("offset can't be negative")
|
|
201
|
+
if whence == os.SEEK_END and offset > 0:
|
|
202
|
+
raise ValueError("offset can't be positive")
|
|
203
|
+
|
|
204
|
+
if whence == os.SEEK_SET:
|
|
205
|
+
self._pos = min(max(offset, 0), self._size)
|
|
206
|
+
elif whence == os.SEEK_CUR:
|
|
207
|
+
self._pos = min(max(self._pos + offset, 0), self._size)
|
|
208
|
+
elif whence == os.SEEK_END:
|
|
209
|
+
self._pos = min(max(self._pos + offset + self._size, 0), self._size)
|
|
210
|
+
return self._pos
|
|
211
|
+
|
|
212
|
+
def tell(self):
|
|
213
|
+
return self._pos
|
|
214
|
+
|
|
215
|
+
def read(self, size=-1):
|
|
216
|
+
res = self.peek(size)
|
|
217
|
+
self.seek(len(res), whence=os.SEEK_CUR)
|
|
218
|
+
return res
|
|
219
|
+
|
|
220
|
+
def peek(self, size=0):
|
|
221
|
+
offset = self._pos
|
|
222
|
+
if offset >= self._size:
|
|
223
|
+
return b""
|
|
224
|
+
|
|
225
|
+
if size <= 0 or offset + size > self._size:
|
|
226
|
+
size = self._size - offset
|
|
227
|
+
|
|
228
|
+
res = bytearray(size)
|
|
229
|
+
for blob, blob_offset, blob_length, f in self._blobs:
|
|
230
|
+
if not range_contains(
|
|
231
|
+
range(offset, offset + size),
|
|
232
|
+
range(blob_offset, blob_offset + blob_length),
|
|
233
|
+
):
|
|
234
|
+
if size >= self._size:
|
|
235
|
+
print(f"Skipping blob {blob_offset} to {blob_length}, {offset}")
|
|
236
|
+
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
blob_data = self._read_blob(blob, blob_offset, blob_length, f)
|
|
240
|
+
blob_start_offset = max(offset - blob_offset, 0)
|
|
241
|
+
blob_end_offset = min(offset - blob_offset + size, blob_length)
|
|
242
|
+
data = blob_data[blob_start_offset:blob_end_offset]
|
|
243
|
+
|
|
244
|
+
assert (
|
|
245
|
+
blob_start_offset >= 0
|
|
246
|
+
), f"blob start offset is negative number: {blob_start_offset}"
|
|
247
|
+
assert (
|
|
248
|
+
blob_end_offset <= blob_length
|
|
249
|
+
), f"blob end offset is larger than blob length: {blob_end_offset}, {blob_length}"
|
|
250
|
+
assert blob_end_offset - blob_start_offset == len(
|
|
251
|
+
data
|
|
252
|
+
), f"blob start and end is larger than data: {blob_end_offset - blob_start_offset}, {len(data)}"
|
|
253
|
+
|
|
254
|
+
start_offset = blob_offset + blob_start_offset - offset
|
|
255
|
+
end_offset = blob_offset + blob_end_offset - offset
|
|
256
|
+
res[start_offset:end_offset] = data
|
|
257
|
+
|
|
258
|
+
assert start_offset >= 0, f"start offset is negative number: {start_offset}"
|
|
259
|
+
assert start_offset < len(
|
|
260
|
+
res
|
|
261
|
+
), f"start offset is larger than size of data: {start_offset}, {len(res)}"
|
|
262
|
+
assert (
|
|
263
|
+
end_offset <= blob_offset + blob_length
|
|
264
|
+
), f"end offset is larger than size of blob: {end_offset}, {blob_offset + blob_length}"
|
|
265
|
+
assert end_offset - start_offset == len(
|
|
266
|
+
data
|
|
267
|
+
), f"size of offsets does not equal size of data, {end_offset - start_offset}, {len(data)}"
|
|
268
|
+
assert end_offset <= len(
|
|
269
|
+
res
|
|
270
|
+
), f"end offset is larger than size of data, {end_offset}, {len(res)}"
|
|
271
|
+
assert res[start_offset:end_offset] == data, "data does not match"
|
|
272
|
+
|
|
273
|
+
assert (
|
|
274
|
+
len(res) == size
|
|
275
|
+
), f"size of data does not match expected size: {len(res)}, {size}"
|
|
276
|
+
return bytes(res)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: update_metadata.proto
|
|
4
|
+
# Protobuf Python Version: 4.25.3
|
|
5
|
+
"""Generated protocol buffer code."""
|
|
6
|
+
from google.protobuf import descriptor as _descriptor
|
|
7
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
8
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
9
|
+
from google.protobuf.internal import builder as _builder
|
|
10
|
+
# @@protoc_insertion_point(imports)
|
|
11
|
+
|
|
12
|
+
_sym_db = _symbol_database.Default()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15update_metadata.proto\x12\x16\x63hromeos_update_engine\"\xe0\x02\n\x10InstallOperation\x12;\n\x04type\x18\x01 \x02(\x0e\x32-.chromeos_update_engine.InstallOperation.Type\x12\x13\n\x0b\x64\x61ta_offset\x18\x02 \x01(\r\x12\x13\n\x0b\x64\x61ta_length\x18\x03 \x01(\r\x12\x33\n\x0bsrc_extents\x18\x04 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\nsrc_length\x18\x05 \x01(\x04\x12\x33\n\x0b\x64st_extents\x18\x06 \x03(\x0b\x32\x1e.chromeos_update_engine.Extent\x12\x12\n\ndst_length\x18\x07 \x01(\x04\x12\x18\n\x10\x64\x61ta_sha256_hash\x18\x08 \x01(\x0c\"9\n\x04Type\x12\x0b\n\x07REPLACE\x10\x00\x12\x0e\n\nREPLACE_BZ\x10\x01\x12\x08\n\x04MOVE\x10\x02\x12\n\n\x06\x42SDIFF\x10\x03\"1\n\x06\x45xtent\x12\x13\n\x0bstart_block\x18\x01 \x01(\x04\x12\x12\n\nnum_blocks\x18\x02 \x01(\x04\"z\n\nSignatures\x12@\n\nsignatures\x18\x01 \x03(\x0b\x32,.chromeos_update_engine.Signatures.Signature\x1a*\n\tSignature\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\")\n\x0bInstallInfo\x12\x0c\n\x04size\x18\x01 \x01(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\x0c\"\x8f\x02\n\x10InstallProcedure\x12;\n\x04type\x18\x01 \x01(\x0e\x32-.chromeos_update_engine.InstallProcedure.Type\x12<\n\noperations\x18\x02 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x35\n\x08old_info\x18\x03 \x01(\x0b\x32#.chromeos_update_engine.InstallInfo\x12\x35\n\x08new_info\x18\x04 \x01(\x0b\x32#.chromeos_update_engine.InstallInfo\"\x12\n\x04Type\x12\n\n\x06KERNEL\x10\x00\"\xaf\x03\n\x14\x44\x65ltaArchiveManifest\x12\x46\n\x14partition_operations\x18\x01 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x41\n\x0fnoop_operations\x18\x02 \x03(\x0b\x32(.chromeos_update_engine.InstallOperation\x12\x18\n\nblock_size\x18\x03 \x01(\r:\x04\x34\x30\x39\x36\x12\x19\n\x11signatures_offset\x18\x04 \x01(\x04\x12\x17\n\x0fsignatures_size\x18\x05 \x01(\x04\x12?\n\x12old_partition_info\x18\x08 \x01(\x0b\x32#.chromeos_update_engine.InstallInfo\x12?\n\x12new_partition_info\x18\t \x01(\x0b\x32#.chromeos_update_engine.InstallInfo\x12<\n\nprocedures\x18\n \x03(\x0b\x32(.chromeos_update_engine.InstallProcedure')
|
|
18
|
+
|
|
19
|
+
_globals = globals()
|
|
20
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
21
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'update_metadata_pb2', _globals)
|
|
22
|
+
if _descriptor._USE_C_DESCRIPTORS == False:
|
|
23
|
+
DESCRIPTOR._options = None
|
|
24
|
+
_globals['_INSTALLOPERATION']._serialized_start=50
|
|
25
|
+
_globals['_INSTALLOPERATION']._serialized_end=402
|
|
26
|
+
_globals['_INSTALLOPERATION_TYPE']._serialized_start=345
|
|
27
|
+
_globals['_INSTALLOPERATION_TYPE']._serialized_end=402
|
|
28
|
+
_globals['_EXTENT']._serialized_start=404
|
|
29
|
+
_globals['_EXTENT']._serialized_end=453
|
|
30
|
+
_globals['_SIGNATURES']._serialized_start=455
|
|
31
|
+
_globals['_SIGNATURES']._serialized_end=577
|
|
32
|
+
_globals['_SIGNATURES_SIGNATURE']._serialized_start=535
|
|
33
|
+
_globals['_SIGNATURES_SIGNATURE']._serialized_end=577
|
|
34
|
+
_globals['_INSTALLINFO']._serialized_start=579
|
|
35
|
+
_globals['_INSTALLINFO']._serialized_end=620
|
|
36
|
+
_globals['_INSTALLPROCEDURE']._serialized_start=623
|
|
37
|
+
_globals['_INSTALLPROCEDURE']._serialized_end=894
|
|
38
|
+
_globals['_INSTALLPROCEDURE_TYPE']._serialized_start=876
|
|
39
|
+
_globals['_INSTALLPROCEDURE_TYPE']._serialized_end=894
|
|
40
|
+
_globals['_DELTAARCHIVEMANIFEST']._serialized_start=897
|
|
41
|
+
_globals['_DELTAARCHIVEMANIFEST']._serialized_end=1328
|
|
42
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: remarkable_update_image
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Read reMarkable update images
|
|
5
|
+
Author-email: Eeems <eeems@eeems.email>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Eeems/remarkable-update-image
|
|
7
|
+
Project-URL: Repository, https://github.com/Eeems/remarkable-update-image.git
|
|
8
|
+
Project-URL: Issues, https://github.com/Eeems/remarkable-update-image/issues
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: System :: Filesystems
|
|
19
|
+
Classifier: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: cryptography==41.0.7
|
|
24
|
+
Requires-Dist: protobuf==4.21.1
|
|
25
|
+
Requires-Dist: ext4==1.0.2
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/remarkable_update_image)
|
|
28
|
+
|
|
29
|
+
# reMarkable Update Image
|
|
30
|
+
Read a reMarkable update image as a block device.
|
|
31
|
+
|
|
32
|
+
## Known Issues
|
|
33
|
+
|
|
34
|
+
- Will report checksum errors for Directory inode, even though they are fine
|
|
35
|
+
- Will report checksum errors for extent headers, even though they are fine
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from ext4 import Volume
|
|
41
|
+
from remarkable_update_image import UpdateImage
|
|
42
|
+
|
|
43
|
+
image = UpdateImage("path/to/update/file.signed")
|
|
44
|
+
|
|
45
|
+
# Extract raw ext4 image
|
|
46
|
+
with open("image.ext4", "wb") as f:
|
|
47
|
+
f.write(image.read())
|
|
48
|
+
|
|
49
|
+
# Extract specific file
|
|
50
|
+
volume = Volume(image)
|
|
51
|
+
inode = volume.inode_at("/etc/version")
|
|
52
|
+
with open("version", "wb") as f:
|
|
53
|
+
f.write(inode.open().read())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Building
|
|
57
|
+
Dependencies:
|
|
58
|
+
- curl
|
|
59
|
+
- protoc
|
|
60
|
+
- python
|
|
61
|
+
- python-build
|
|
62
|
+
- python-pip
|
|
63
|
+
- python-pipx
|
|
64
|
+
- python-venv
|
|
65
|
+
- python-wheel
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
make # Build wheel and sdist packages in dist/
|
|
69
|
+
make wheel # Build wheel package in dist/
|
|
70
|
+
make sdist # Build sdist package in dist/
|
|
71
|
+
make test # Run unit tests
|
|
72
|
+
make install # Build wheel and install it with pipx or pip install --user
|
|
73
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements.txt
|
|
5
|
+
remarkable_update_image/__init__.py
|
|
6
|
+
remarkable_update_image/image.py
|
|
7
|
+
remarkable_update_image/update_metadata_pb2.py
|
|
8
|
+
remarkable_update_image.egg-info/PKG-INFO
|
|
9
|
+
remarkable_update_image.egg-info/SOURCES.txt
|
|
10
|
+
remarkable_update_image.egg-info/dependency_links.txt
|
|
11
|
+
remarkable_update_image.egg-info/requires.txt
|
|
12
|
+
remarkable_update_image.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
remarkable_update_image
|