1
0
mirror of https://codeberg.org/fediverse/fep.git synced 2026-08-05 11:46:04 +00:00

FEP-d8c8: BitTorrent Torrent Objects (#714)

Related to our plans with [sciop](https://sciop.net) becoming a federated bittorrent tracker, this very simple FEP describes how to represent torrents via ActivityStreams :)

discussion: https://socialhub.activitypub.rocks/t/fep-d8c8-bittorrent-torrent-objects/8309

fedi thread: https://neuromatch.social/@jonny/115488792572987927

Co-authored-by: silverpill <silverpill@noreply.codeberg.org>
Co-authored-by: helge <helge@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/fediverse/fep/pulls/714
Co-authored-by: sneakers-the-rat <sneakers-the-rat@protonmail.com>
Co-committed-by: sneakers-the-rat <sneakers-the-rat@protonmail.com>
This commit is contained in:
sneakers-the-rat
2025-11-06 23:40:36 +01:00
committed by silverpill
parent b9657a1c71
commit 6d2865446b
13 changed files with 1612 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
.pdm.toml
.pdm-python
.pdm-build/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
__tmp__/
+9
View File
@@ -0,0 +1,9 @@
{
"@context": {
"Torrent": "https://w3id.org/fep/d8c8#Torrent",
"infohash_v1": "https://w3id.org/fep/d8c8#infohash_v1",
"infohash_v2: "https://w3id.org/fep/d8c8#infohash_v2",
"magnet": "https://w3id.org/fep/d8c8#magnet",
"bencoded": "https://w3id.org/fep/d8c8#bencoded",
}
}
+1037
View File
File diff suppressed because one or more lines are too long
+36
View File
@@ -0,0 +1,36 @@
# FEP d8c8 Reference Implementation
## Usage
Install the package with `python -m pip install .`
Use the CLI `fepd8c8`, call `fepd8c8 --help` for usage instructions
```
usage: fepd8c8 [-h] [--encode] [--decode] [--with-context] -i INPUT [-o OUTPUT]
Encoding and decoding torrents to ActivityStreams Torrent objects.
options:
-h, --help show this help message and exit
--encode Encode a .torrent file to JSON
--decode Decode a JSON torrent object to .torrent
--with-context Include the JSON-LD Context in the json output
-i, --input INPUT path to input .torrent or JSON file
-o, --output OUTPUT path to write output to. If not provided, print to stdout
```
## Testing
To validate that we can roundtrip a torrent to/from the JSON representation,
- install with the optional `test` dependency group: `python -m pip install '.[test]'`
- call pytest with a `--torrentdir ./some/directory` argument
that points to a directory with .torrent files to roundtrip
- Optionally: call with `--keep-output` to inspect the created json files
## See Also
[`torrent-models`](https://pypi.org/project/torrent-models/) -
a more complete implementation of .torrent files in python
+207
View File
@@ -0,0 +1,207 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
lock-version = "1.0"
requires-python = ">=3.10"
environments = [
"python_version >= \"3.10\"",
]
extras = ["test"]
dependency-groups = ["default"]
default-groups = ["default"]
created-by = "pdm"
[[packages]]
name = "bencode-rs"
version = "0.0.7"
requires-python = "~=3.10"
sdist = {name = "bencode_rs-0.0.7.tar.gz", url = "https://files.pythonhosted.org/packages/db/f5/091727719e840af53ff2e30cad673ba1ccc3fef86dc5ea2390499d21c7bd/bencode_rs-0.0.7.tar.gz", hashes = {sha256 = "ac975fe2901154b6882411fdafe44fd0fd54c21bca6c9f546941336482c8bd7b"}}
wheels = [
{name = "bencode_rs-0.0.7-cp310-abi3-macosx_10_12_x86_64.whl",url = "https://files.pythonhosted.org/packages/0f/b9/776ea10006cd4442d68bfea4bdca981b3e72587316dfb80551ce48663889/bencode_rs-0.0.7-cp310-abi3-macosx_10_12_x86_64.whl",hashes = {sha256 = "d446d7fce52e135c65b5e15cdaef5fc64c39aa4215bccb4b803676df05a3ab45"}},
{name = "bencode_rs-0.0.7-cp310-abi3-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/b5/da/eb12d4ff850032b24a913d704116bc318c5a974ccaa5f15269e3337a6bfa/bencode_rs-0.0.7-cp310-abi3-macosx_11_0_arm64.whl",hashes = {sha256 = "89de74e7b34eabe20c5a8ba58cabc75764ec881a0b1dae02b1d7bcb6b2a5ce6c"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",url = "https://files.pythonhosted.org/packages/35/41/360fb61a132436f64c26f8b2ab379980439d569eceba4e48ff3499de2b1f/bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl",hashes = {sha256 = "354a30aa7c840684f7e48ea8b3f598f0e4c8dcbe77297ce040f92081396107fd"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",url = "https://files.pythonhosted.org/packages/84/2e/2f3cf968f8381991583382976fe953942dfe82b995f3aa75df3d53f11a62/bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl",hashes = {sha256 = "24df67362897e3b638ac7e4830fca37059d3372d3ece2973eba06506ca7a4bbe"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",url = "https://files.pythonhosted.org/packages/78/a4/eb1c96d4ca749f8b50b6046597cfe5cd9456800800f2dc50a852a6cf72d5/bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl",hashes = {sha256 = "08913a6b89e7d9fdf29d3d2582a1e4b769695a4dff643a85cdcc5bd1c4de1f72"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl",url = "https://files.pythonhosted.org/packages/ab/62/efbfd79c3c72747354cba385e1a167017fc090e7d3d88a1bf01475d0d2e5/bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl",hashes = {sha256 = "c367ec0313da98f85e8f2bd1f8a659cc1a89a91355e658a6db4b84dbb848460b"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",url = "https://files.pythonhosted.org/packages/9f/2c/3900449623c33514f60639e91c559c6307cb2e598902c4afbb5099b60cd9/bencode_rs-0.0.7-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",hashes = {sha256 = "8aa3bbfb15fa67b49a7afb138344b22b2ab36333f9dc640aa8e002b254975621"}},
{name = "bencode_rs-0.0.7-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl",url = "https://files.pythonhosted.org/packages/2c/0d/219db7e13c86b8e1a9fa91ac4489fdd01e26bd51a39a57b2b0271b87165a/bencode_rs-0.0.7-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl",hashes = {sha256 = "1393e3c66f2b697e2b7d9f40c2720e1812b410b3536ceaa1d0170bd0636b286a"}},
{name = "bencode_rs-0.0.7-cp310-abi3-win32.whl",url = "https://files.pythonhosted.org/packages/d6/c2/bc67dbc082c709fb33fc4c51bd5cc95ba29221b3cf96656788dd8f4ccaa0/bencode_rs-0.0.7-cp310-abi3-win32.whl",hashes = {sha256 = "17975cc8b040c05301ced06cbda5dbd2b2d6c2b45e5e2164f0fb35774c66c5eb"}},
{name = "bencode_rs-0.0.7-cp310-abi3-win_amd64.whl",url = "https://files.pythonhosted.org/packages/78/11/5c8d779d7d1138446a669f3f0f4e07148cb193fd6c78245ad547b3367465/bencode_rs-0.0.7-cp310-abi3-win_amd64.whl",hashes = {sha256 = "723c611af79972fc19aca5482f3c1372613e2bbfd618714f336578749db36080"}},
]
marker = "\"default\" in dependency_groups"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "pytest"
version = "8.4.2"
requires-python = ">=3.9"
sdist = {name = "pytest-8.4.2.tar.gz", url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hashes = {sha256 = "86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}}
wheels = [
{name = "pytest-8.4.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl",hashes = {sha256 = "872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}},
]
marker = "\"test\" in extras"
[packages.tool.pdm]
dependencies = [
"colorama>=0.4; sys_platform == \"win32\"",
"exceptiongroup>=1; python_version < \"3.11\"",
"iniconfig>=1",
"packaging>=20",
"pluggy<2,>=1.5",
"pygments>=2.7.2",
"tomli>=1; python_version < \"3.11\"",
]
[[packages]]
name = "pluggy"
version = "1.6.0"
requires-python = ">=3.9"
sdist = {name = "pluggy-1.6.0.tar.gz", url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hashes = {sha256 = "7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}}
wheels = [
{name = "pluggy-1.6.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl",hashes = {sha256 = "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}},
]
marker = "\"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "colorama"
version = "0.4.6"
requires-python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
sdist = {name = "colorama-0.4.6.tar.gz", url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hashes = {sha256 = "08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}}
wheels = [
{name = "colorama-0.4.6-py2.py3-none-any.whl",url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",hashes = {sha256 = "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}},
]
marker = "sys_platform == \"win32\" and \"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "exceptiongroup"
version = "1.3.0"
requires-python = ">=3.7"
sdist = {name = "exceptiongroup-1.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hashes = {sha256 = "b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}}
wheels = [
{name = "exceptiongroup-1.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl",hashes = {sha256 = "4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}},
]
marker = "python_version < \"3.11\" and \"test\" in extras"
[packages.tool.pdm]
dependencies = [
"typing-extensions>=4.6.0; python_version < \"3.13\"",
]
[[packages]]
name = "iniconfig"
version = "2.3.0"
requires-python = ">=3.10"
sdist = {name = "iniconfig-2.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hashes = {sha256 = "c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}}
wheels = [
{name = "iniconfig-2.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl",hashes = {sha256 = "f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}},
]
marker = "\"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "packaging"
version = "25.0"
requires-python = ">=3.8"
sdist = {name = "packaging-25.0.tar.gz", url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hashes = {sha256 = "d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}}
wheels = [
{name = "packaging-25.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl",hashes = {sha256 = "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}},
]
marker = "\"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "pygments"
version = "2.19.2"
requires-python = ">=3.8"
sdist = {name = "pygments-2.19.2.tar.gz", url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hashes = {sha256 = "636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}}
wheels = [
{name = "pygments-2.19.2-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl",hashes = {sha256 = "86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}},
]
marker = "\"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "tomli"
version = "2.3.0"
requires-python = ">=3.8"
sdist = {name = "tomli-2.3.0.tar.gz", url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hashes = {sha256 = "64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}}
wheels = [
{name = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl",hashes = {sha256 = "cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}},
{name = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl",hashes = {sha256 = "4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}},
{name = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}},
{name = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}},
{name = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl",hashes = {sha256 = "b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}},
{name = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl",hashes = {sha256 = "b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}},
{name = "tomli-2.3.0-cp314-cp314-win32.whl",url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl",hashes = {sha256 = "feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}},
{name = "tomli-2.3.0-cp314-cp314-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl",hashes = {sha256 = "b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}},
{name = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl",hashes = {sha256 = "940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}},
{name = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl",hashes = {sha256 = "f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}},
{name = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}},
{name = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}},
{name = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl",hashes = {sha256 = "5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}},
{name = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl",hashes = {sha256 = "8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}},
{name = "tomli-2.3.0-cp314-cp314t-win32.whl",url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl",hashes = {sha256 = "a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}},
{name = "tomli-2.3.0-cp314-cp314t-win_amd64.whl",url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl",hashes = {sha256 = "70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}},
{name = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl",hashes = {sha256 = "5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}},
{name = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl",hashes = {sha256 = "be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}},
{name = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}},
{name = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}},
{name = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl",hashes = {sha256 = "a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}},
{name = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl",hashes = {sha256 = "ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}},
{name = "tomli-2.3.0-cp313-cp313-win32.whl",url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl",hashes = {sha256 = "97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}},
{name = "tomli-2.3.0-cp313-cp313-win_amd64.whl",url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl",hashes = {sha256 = "0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}},
{name = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl",url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl",hashes = {sha256 = "d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}},
{name = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl",hashes = {sha256 = "73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}},
{name = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}},
{name = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}},
{name = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl",hashes = {sha256 = "e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}},
{name = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl",hashes = {sha256 = "7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}},
{name = "tomli-2.3.0-cp312-cp312-win32.whl",url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl",hashes = {sha256 = "ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}},
{name = "tomli-2.3.0-cp312-cp312-win_amd64.whl",url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl",hashes = {sha256 = "1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}},
{name = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl",url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl",hashes = {sha256 = "88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}},
{name = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl",url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl",hashes = {sha256 = "883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}},
{name = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl",hashes = {sha256 = "d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}},
{name = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl",hashes = {sha256 = "a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}},
{name = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl",hashes = {sha256 = "0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}},
{name = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl",hashes = {sha256 = "74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}},
{name = "tomli-2.3.0-cp311-cp311-win32.whl",url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl",hashes = {sha256 = "00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}},
{name = "tomli-2.3.0-cp311-cp311-win_amd64.whl",url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl",hashes = {sha256 = "4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}},
{name = "tomli-2.3.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl",hashes = {sha256 = "e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}},
]
marker = "python_version < \"3.11\" and \"test\" in extras"
[packages.tool.pdm]
dependencies = []
[[packages]]
name = "typing-extensions"
version = "4.15.0"
requires-python = ">=3.9"
sdist = {name = "typing_extensions-4.15.0.tar.gz", url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hashes = {sha256 = "0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}}
wheels = [
{name = "typing_extensions-4.15.0-py3-none-any.whl",url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl",hashes = {sha256 = "f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}},
]
marker = "python_version < \"3.11\" and \"test\" in extras"
[packages.tool.pdm]
dependencies = []
[tool.pdm]
hashes = {sha256 = "88219edbe008786cb97c987ee642dc0af3aeec22c8662b24a35ca8d20f19b37b"}
strategy = ["inherit_metadata", "static_urls"]
[[tool.pdm.targets]]
requires_python = ">=3.10"
+26
View File
@@ -0,0 +1,26 @@
[project]
name = "fepd8c8"
version = "0.1.0"
description = "Reference implementation for FEP-d8c8"
authors = [
{name = "sneakers-the-rat", email = "sneakers-the-rat@protonmail.com"},
]
dependencies = ["bencode-rs>=0.0.7"]
requires-python = ">=3.10"
readme = "README.md"
license = {text = "CC-0"}
[project.scripts]
fepd8c8 = "fepd8c8.cli:main"
[project.optional-dependencies]
test = [
"pytest>=8.4.2",
]
[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"
[tool.pdm]
distribution = true
@@ -0,0 +1,6 @@
from fepd8c8.torrent import decode_json, encode_torrent
__all__ = [
"decode_json",
"encode_torrent",
]
@@ -0,0 +1,3 @@
from fepd8c8.cli import main
main()
@@ -0,0 +1,53 @@
from argparse import ArgumentParser
from pathlib import Path
import json
from fepd8c8.torrent import encode_torrent, decode_json
def parser() -> ArgumentParser:
parser = ArgumentParser(
prog="fepd8c8",
description="Encoding and decoding torrents to ActivityStreams Torrent objects.",
)
parser.add_argument(
"--encode", help="Encode a .torrent file to JSON", action="store_true"
)
parser.add_argument(
"--decode", help="Decode a JSON torrent object to .torrent", action="store_true"
)
parser.add_argument(
"--with-context",
help="Include the JSON-LD Context in the json output",
action="store_true",
)
parser.add_argument(
"-i", "--input", help="path to input .torrent or JSON file", required=True
)
parser.add_argument(
"-o",
"--output",
help="path to write output to. If not provided, print to stdout",
)
return parser
def main():
args = parser().parse_args()
in_path = Path(args.input)
if args.encode:
val = encode_torrent(in_path, with_context=args.with_context)
elif args.decode:
val = decode_json(in_path)
else:
raise ValueError("Must specify whether decoding or encoding")
if args.output:
out_path = Path(args.output)
if args.encode:
with open(out_path, "w") as f:
json.dump(val, f, indent=2)
else:
out_path.write_bytes(val)
else:
print(val)
@@ -0,0 +1,159 @@
"""
Convert a .torrent file to a FEP-d8c8 JSON representation
"""
import base64
import hashlib
from pathlib import Path
import json
from typing import Literal, overload
from bencode_rs import bencode, bdecode
CONTEXT = ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/d8c8"]
B64_KEYS = ("pieces",)
"""Keys whose values should be encoded as base64"""
HEX_KEYS = (
"infohash_v1",
"infohash_v2",
"pieces root",
)
"""Keys whose values should be encoded as hex"""
EXTRA_KEYS = (
"@context",
"infohash_v1",
"infohash_v2",
"type",
)
"""Extra keys in the JSON form that should be stripped out of the bencoded form"""
@overload
def _handle_piece_layers(
value: dict[bytes, bytes], mode: Literal["encode"] = "encode"
) -> dict[str, str]: ...
@overload
def _handle_piece_layers(
value: dict[str, str], mode: Literal["decode"] = "decode"
) -> dict[bytes, bytes]: ...
def _handle_piece_layers(
value: dict[bytes, bytes] | dict[str, str],
mode: Literal["encode", "decode"] = "encode",
) -> dict[str, str] | dict[bytes, bytes]:
"""Encode keys as hex, values as b64"""
if mode == "encode":
return {k.hex(): base64.b64encode(v).decode("utf-8") for k, v in value.items()}
elif mode == "decode":
return {bytes.fromhex(k): base64.b64decode(v) for k, v in value.items()}
else:
raise ValueError("Unknown piece layers mode")
SPECIAL_KEYS = {"piece layers": _handle_piece_layers}
"""Map of keys whose values should be handled with some special handler"""
def encode_strings(value: dict | list) -> dict | list:
"""
Encode strings as unicode, hex, or base64 strings according to the FEP.
"""
if isinstance(value, dict):
new_value = {}
for k, v in value.items():
if isinstance(k, bytes):
k = k.decode("utf-8")
if k in SPECIAL_KEYS:
new_value[k] = SPECIAL_KEYS[k](v, mode="encode")
elif isinstance(v, bytes):
if k in B64_KEYS:
new_value[k] = base64.b64encode(v).decode("utf-8")
elif k in HEX_KEYS:
new_value[k] = v.hex()
else:
new_value[k] = v.decode("utf-8")
elif isinstance(v, list | dict):
new_value[k] = encode_strings(v)
else:
new_value[k] = v
elif isinstance(value, list):
new_value = [encode_strings(item) for item in value]
elif isinstance(value, bytes):
# should only encounter this while iterating over a list really
new_value = value.decode("utf-8")
else:
new_value = value
return new_value
def decode_strings(value: dict | list) -> dict | list:
"""
Decode strings back into bencodable binary
"""
if isinstance(value, dict):
new_value = {}
for k, v in value.items():
if isinstance(k, bytes):
k_bytes = k
k = k.decode("utf-8")
else:
k_bytes = k.encode("utf-8")
if k in SPECIAL_KEYS:
new_value[k_bytes] = SPECIAL_KEYS[k](v, mode="decode")
elif isinstance(v, str):
if k in B64_KEYS:
new_value[k_bytes] = base64.b64decode(v)
elif k in HEX_KEYS:
new_value[k_bytes] = bytes.fromhex(v)
else:
new_value[k_bytes] = v.encode("utf-8")
elif isinstance(v, list | dict):
new_value[k] = decode_strings(v)
else:
new_value[k] = v
elif isinstance(value, list):
new_value = [decode_strings(item) for item in value]
elif isinstance(value, str):
new_value = value.encode("utf-8")
else:
new_value = value
return new_value
def make_infohashes(info: dict) -> dict[str, str]:
encoded_infodict = bencode(info)
ret = {}
if b"pieces" in info:
ret["infohash_v1"] = hashlib.sha1(encoded_infodict).hexdigest()
if b"file tree" in info:
ret["infohash_v2"] = hashlib.sha256(encoded_infodict).hexdigest()
return ret
def encode_torrent(torrent: Path | bytes, with_context: bool = False) -> dict:
"""Encode a bencoded torrent to json"""
data = torrent.read_bytes() if isinstance(torrent, Path) else torrent
bdecoded = bdecode(data)
encoded = encode_strings(bdecoded)
encoded.update(make_infohashes(bdecoded[b"info"]))
encoded["type"] = "Torrent"
if with_context:
encoded["@context"] = CONTEXT
return encoded
def decode_json(value: Path | dict) -> bytes:
"""Decode json to bencoded binary"""
data = json.loads(value.read_text()) if isinstance(value, Path) else value
# remove infohashes
data = {k: v for k, v in data.items() if k not in EXTRA_KEYS}
decoded = decode_strings(data)
return bencode(decoded)
+25
View File
@@ -0,0 +1,25 @@
import pytest
import warnings
from pathlib import Path
def pytest_addoption(parser):
parser.addoption(
"--torrentdir",
help="directory of torrent files to roundtrip",
action="store",
default=None,
)
parser.addoption(
"--keep-output", help="Keep temporary JSON files", action="store_true"
)
def pytest_generate_tests(metafunc):
if "torrent_path" in metafunc.fixturenames:
torrent_dir = metafunc.config.getoption("torrentdir")
if not torrent_dir:
warnings.warn("No torrent-dir passed, no torrents will be tested!")
else:
torrents = list(Path(torrent_dir).rglob("**/*.torrent"))
metafunc.parametrize("torrent_path", torrents)
@@ -0,0 +1,32 @@
from pathlib import Path
import pytest
import json
from fepd8c8 import encode_torrent, decode_json
TMP_DIR = Path(__file__).parent / "__tmp__"
@pytest.mark.parametrize("context", [True, False])
def test_roundtrip(
torrent_path: Path, context: bool, request: pytest.FixtureRequest, tmp_path: Path
):
"""We can roundtrip a torrent to and from JSON"""
data = torrent_path.read_bytes()
as_dict = encode_torrent(data, with_context=context)
# ensure valid json
as_json_str = json.dumps(as_dict, indent=2)
# dump to path
tmp_json = (tmp_path / torrent_path.name).with_suffix(".json")
tmp_json.write_text(as_json_str)
# write output to an inspectable directory if requested
if request.config.getoption("--keep-output"):
out_dir = TMP_DIR / "with_context" if context else TMP_DIR / "without_context"
out_dir.mkdir(parents=True, exist_ok=True)
out_path = (out_dir / torrent_path.name).with_suffix(".json")
out_path.write_text(as_json_str, encoding="utf-8")
# invert back to bencoded form
re_bencoded = decode_json(tmp_json)
assert re_bencoded == data