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.
@@ -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
+ [![remarkable_update_image on PyPI](https://img.shields.io/pypi/v/remarkable_update_image)](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
+ [![remarkable_update_image on PyPI](https://img.shields.io/pypi/v/remarkable_update_image)](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,3 @@
1
+ from .image import UpdateImage
2
+ from .image import UpdateImageException
3
+ from .image import UpdateImageSignatureException
@@ -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
+ [![remarkable_update_image on PyPI](https://img.shields.io/pypi/v/remarkable_update_image)](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,3 @@
1
+ cryptography==41.0.7
2
+ protobuf==4.21.1
3
+ ext4==1.0.2
@@ -0,0 +1 @@
1
+ remarkable_update_image
@@ -0,0 +1,3 @@
1
+ cryptography==41.0.7
2
+ protobuf==4.21.1
3
+ ext4==1.0.2
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+