flatback 1.0__tar.gz → 1.1__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.
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatback
3
- Version: 1.0
3
+ Version: 1.1
4
4
  Summary: Backup and restore Flatpak applications for offline transfer
5
- Home-page: https://github.com/jfhack/flatback
6
5
  Project-URL: Homepage, https://github.com/jfhack/flatback
7
6
  Requires-Python: >=3.8
8
7
  Description-Content-Type: text/markdown
9
8
  Requires-Dist: pyyaml
10
- Dynamic: home-page
11
9
 
12
10
  # Flatback
13
11
 
@@ -52,6 +50,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
52
50
 
53
51
  **Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
54
52
 
53
+ > **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
54
+
55
55
  ### Manifest Example (`flatback.yml`)
56
56
 
57
57
  ```yaml
@@ -41,6 +41,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
41
41
 
42
42
  **Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
43
43
 
44
+ > **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
45
+
44
46
  ### Manifest Example (`flatback.yml`)
45
47
 
46
48
  ```yaml
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import subprocess
2
3
  import yaml
3
4
  import shutil
@@ -8,6 +9,25 @@ from pathlib import Path
8
9
  from . import __version__
9
10
 
10
11
 
12
+ _APP_ID_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_-][A-Za-z0-9_-]*){2,}")
13
+
14
+
15
+ def is_valid_app_id(app_id):
16
+ return isinstance(app_id, str) and len(app_id) <= 255 and _APP_ID_RE.fullmatch(app_id) is not None
17
+
18
+ def _is_within(path, base):
19
+ try:
20
+ Path(path).relative_to(base)
21
+ return True
22
+ except ValueError:
23
+ return False
24
+
25
+
26
+ def _ignore_special_files(src_dir, names):
27
+ paths = (Path(src_dir) / name for name in names)
28
+ return {p.name for p in paths if not (p.is_symlink() or p.is_dir() or p.is_file())}
29
+
30
+
11
31
  class FlatpakRef:
12
32
  def __init__(self, app_id, arch="x86_64", branch="stable", origin="flathub", installation="user"):
13
33
  self.app_id = app_id
@@ -159,7 +179,7 @@ class Backup:
159
179
  out = Path(output_path or default_name)
160
180
  if out.exists():
161
181
  shutil.rmtree(out)
162
- shutil.copytree(tmp_dir, out)
182
+ shutil.copytree(tmp_dir, out, symlinks=True)
163
183
 
164
184
  print(f"Backup saved to: {out}")
165
185
  return out
@@ -171,7 +191,7 @@ class Backup:
171
191
  return False
172
192
  dst = tmp_dir / self.DATA_DIR
173
193
  print(f"Copying user data: {src}")
174
- shutil.copytree(src, dst, symlinks=True)
194
+ shutil.copytree(src, dst, symlinks=True, ignore=_ignore_special_files)
175
195
  return True
176
196
 
177
197
  def _pack_tar(self, source_dir, tar_path):
@@ -202,7 +222,7 @@ class Restore:
202
222
  with tempfile.TemporaryDirectory() as tmp:
203
223
  tmp_dir = Path(tmp)
204
224
  with tarfile.open(tar_path, "r:gz") as tar:
205
- tar.extractall(tmp_dir, filter="data")
225
+ tar.extractall(tmp_dir, filter=self._data_filter)
206
226
  self._load_from_dir(tmp_dir)
207
227
 
208
228
  def _load_from_dir(self, directory):
@@ -212,12 +232,16 @@ class Restore:
212
232
 
213
233
  with open(manifest_path) as f:
214
234
  manifest = yaml.safe_load(f)
235
+ if not isinstance(manifest, dict):
236
+ raise ValueError(f"Invalid manifest: {manifest_path}")
215
237
 
216
- runtime_bundle = directory / manifest["runtime_bundle"]
217
- app_bundle = directory / manifest["app_bundle"]
238
+ runtime_bundle = self._bundle_path(directory, manifest.get("runtime_bundle"), "runtime_bundle")
239
+ app_bundle = self._bundle_path(directory, manifest.get("app_bundle"), "app_bundle")
218
240
 
219
241
  runtime_ref = FlatpakRef.from_dict(manifest["runtime"])
220
242
  app_ref = FlatpakRef.from_dict(manifest["app"])
243
+ if not is_valid_app_id(app_ref.app_id):
244
+ raise ValueError(f"Invalid app_id in manifest: {app_ref.app_id!r}")
221
245
 
222
246
  print(f"Installing runtime: {runtime_ref.ref}")
223
247
  self._install_bundle(runtime_bundle)
@@ -230,10 +254,44 @@ class Restore:
230
254
 
231
255
  print(f"Restore complete: {app_ref.app_id}")
232
256
 
257
+ @staticmethod
258
+ def _data_filter(member, path):
259
+ try:
260
+ return tarfile.data_filter(member, path)
261
+ except tarfile.FilterError as e:
262
+ print(f"Skipping unsafe archive entry: {e}")
263
+ return None
264
+
265
+ @staticmethod
266
+ def _bundle_path(directory, name, field):
267
+ if not isinstance(name, str) or name in ("", ".", "..") or Path(name).name != name:
268
+ raise ValueError(f"Invalid {field} in manifest: {name!r}")
269
+ path = directory / name
270
+ if not _is_within(path.resolve(), directory.resolve()):
271
+ raise ValueError(f"{field} points outside the backup: {name!r}")
272
+ if not path.is_file():
273
+ raise FileNotFoundError(f"Bundle not found in backup: {path}")
274
+ return path
275
+
276
+ @staticmethod
277
+ def _remove_escaping_symlinks(root):
278
+ root = root.resolve()
279
+ for link in [p for p in root.rglob("*") if p.is_symlink()]:
280
+ try:
281
+ target = link.resolve()
282
+ except (OSError, RuntimeError):
283
+ target = None
284
+ if target is None or not _is_within(target, root):
285
+ print(f"Removed symlink pointing outside the app data: {link.relative_to(root)} -> {target or 'symlink loop'}")
286
+ link.unlink()
287
+
233
288
  def _handle_user_data(self, directory, app_id):
234
289
  data_dir = directory / self.DATA_DIR
235
290
  if not data_dir.exists():
236
291
  return
292
+ if data_dir.is_symlink() or not data_dir.is_dir():
293
+ print(f"Skipping user data: {data_dir} is not a regular directory")
294
+ return
237
295
 
238
296
  should_restore = self._restore_data
239
297
  if should_restore is None:
@@ -249,7 +307,8 @@ class Restore:
249
307
  print(f"Replacing existing user data: {dst}")
250
308
  shutil.rmtree(dst)
251
309
  print(f"Restoring user data to: {dst}")
252
- shutil.copytree(data_dir, dst, symlinks=True)
310
+ shutil.copytree(data_dir, dst, symlinks=True, ignore=_ignore_special_files)
311
+ self._remove_escaping_symlinks(dst)
253
312
 
254
313
  def _install_bundle(self, bundle_path):
255
314
  result = subprocess.run(
@@ -1,13 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatback
3
- Version: 1.0
3
+ Version: 1.1
4
4
  Summary: Backup and restore Flatpak applications for offline transfer
5
- Home-page: https://github.com/jfhack/flatback
6
5
  Project-URL: Homepage, https://github.com/jfhack/flatback
7
6
  Requires-Python: >=3.8
8
7
  Description-Content-Type: text/markdown
9
8
  Requires-Dist: pyyaml
10
- Dynamic: home-page
11
9
 
12
10
  # Flatback
13
11
 
@@ -52,6 +50,8 @@ flatback restore org.popular.emulator.tar.gz --no-data
52
50
 
53
51
  **Restore** extracts the archive, reads the manifest, and installs the runtime and app bundles using `flatpak install --bundle`.
54
52
 
53
+ > **Only restore backups you created or trust.** A backup installs software and can replace `~/.var/app/<app_id>`. As a safeguard, restore rejects manifests whose bundle names are not plain files inside the backup or whose `app_id` is not a valid Flatpak ID, and skips user-data symlinks that point outside `~/.var/app/<app_id>`.
54
+
55
55
  ### Manifest Example (`flatback.yml`)
56
56
 
57
57
  ```yaml
@@ -1,6 +1,5 @@
1
1
  README.md
2
2
  pyproject.toml
3
- setup.py
4
3
  flatback/__init__.py
5
4
  flatback/__main__.py
6
5
  flatback/core.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flatback"
7
- version = "1.0"
7
+ version = "1.1"
8
8
  description = "Backup and restore Flatpak applications for offline transfer"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
flatback-1.0/setup.py DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env python
2
- from setuptools import setup, find_packages
3
-
4
- with open("README.md", "r", encoding="utf-8") as fh:
5
- long_description = fh.read()
6
-
7
- setup(
8
- name="flatback",
9
- version="1.0",
10
- packages=find_packages(),
11
- entry_points={
12
- "console_scripts": [
13
- "flatback = flatback.__main__:main",
14
- ],
15
- },
16
- url="https://github.com/jfhack/flatback",
17
- install_requires=[
18
- "pyyaml",
19
- ],
20
- long_description=long_description,
21
- long_description_content_type="text/markdown",
22
- )
File without changes
File without changes
File without changes