diff --git a/.ci/aptPackagesToInstall.txt b/.ci/aptPackagesToInstall.txt
new file mode 100644
index 0000000..b9ca3b6
--- /dev/null
+++ b/.ci/aptPackagesToInstall.txt
@@ -0,0 +1 @@
+libzip4
diff --git a/.ci/pythonPackagesToInstallFromGit.txt b/.ci/pythonPackagesToInstallFromGit.txt
new file mode 100644
index 0000000..bd79dda
--- /dev/null
+++ b/.ci/pythonPackagesToInstallFromGit.txt
@@ -0,0 +1 @@
+https://github.com/KOLANICH-libs/libzip.py
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c9162b9
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+indent_size = 4
+insert_final_newline = true
+end_of_line = lf
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
diff --git a/.github/.templateMarker b/.github/.templateMarker
new file mode 100644
index 0000000..5e3a3e0
--- /dev/null
+++ b/.github/.templateMarker
@@ -0,0 +1 @@
+KOLANICH/python_project_boilerplate.py
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..89ff339
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ allow:
+ - dependency-type: "all"
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..7fe33b3
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,15 @@
+name: CI
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ build:
+ runs-on: ubuntu-22.04
+ steps:
+ - name: typical python workflow
+ uses: KOLANICH-GHActions/typical-python-workflow@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..89630d5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+__pycache__
+*.pyc
+*.pyo
+/*.egg-info
+*.srctrlbm
+*.srctrldb
+build
+dist
+.eggs
+monkeytype.sqlite3
+/.ipynb_checkpoints
+omni.ja
+/omni
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..8d4d1b3
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,51 @@
+image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest
+
+variables:
+ DOCKER_DRIVER: overlay2
+ SAST_ANALYZER_IMAGE_TAG: latest
+ SAST_DISABLE_DIND: "true"
+ SAST_CONFIDENCE_LEVEL: 5
+ CODECLIMATE_VERSION: latest
+
+include:
+ - template: SAST.gitlab-ci.yml
+ - template: Code-Quality.gitlab-ci.yml
+ - template: License-Management.gitlab-ci.yml
+
+build:
+ tags:
+ - shared
+ - linux
+ stage: build
+ variables:
+ GIT_DEPTH: "1"
+ PYTHONUSERBASE: ${CI_PROJECT_DIR}/python_user_packages
+
+ before_script:
+ - export PATH="$PATH:$PYTHONUSERBASE/bin" # don't move into `variables`
+ - apt-get update
+ # todo:
+ #- apt-get -y install
+ #- pip3 install --upgrade
+ #- python3 ./fix_python_modules_paths.py
+
+ script:
+ - python3 -m build -nw bdist_wheel
+ - mv ./dist/*.whl ./dist/firefucks-0.CI-py3-none-any.whl
+ - pip3 install --upgrade ./dist/*.whl
+ - coverage run --source=firefucks -m --branch pytest --junitxml=./rspec.xml ./tests/test.py
+ - coverage report -m
+ - coverage xml
+
+ coverage: "/^TOTAL(?:\\s+\\d+){4}\\s+(\\d+%).+/"
+
+ cache:
+ paths:
+ - $PYTHONUSERBASE
+
+ artifacts:
+ paths:
+ - dist
+ reports:
+ junit: ./rspec.xml
+ cobertura: ./coverage.xml
diff --git a/Code_Of_Conduct.md b/Code_Of_Conduct.md
new file mode 100644
index 0000000..bcaa2bf
--- /dev/null
+++ b/Code_Of_Conduct.md
@@ -0,0 +1 @@
+No codes of conduct!
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..20f0fa8
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include UNLICENSE
+include *.md
+include tests
+include .editorconfig
diff --git a/ReadMe.md b/ReadMe.md
new file mode 100644
index 0000000..5756348
--- /dev/null
+++ b/ReadMe.md
@@ -0,0 +1,106 @@
+firefucks.py [![Unlicensed work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/)
+============
+~~[wheel (GitLab)](https://gitlab.com/KOLANICH-tools/firefucks.py/-/jobs/artifacts/master/raw/dist/firefucks-0.CI-py3-none-any.whl?job=build)~~
+[wheel (GHA via `nightly.link`)](https://nightly.link/KOLANICH-tools/firefucks.py/workflows/CI/master/firefucks-0.CI-py3-none-any.whl)
+~~![GitLab Build Status](https://gitlab.com/KOLANICH-tools/firefucks.py/badges/master/pipeline.svg)~~
+~~![GitLab Coverage](https://gitlab.com/KOLANICH-tools/firefucks.py/badges/master/coverage.svg)~~
+[![GitHub Actions](https://github.com/KOLANICH-tools/firefucks.py/workflows/CI/badge.svg)](https://github.com/KOLANICH-tools/firefucks.py/actions/)
+[![Libraries.io Status](https://img.shields.io/librariesio/github/KOLANICH-tools/firefucks.py.svg)](https://libraries.io/github/KOLANICH-tools/firefucks.py)
+[![Code style: antiflash](https://img.shields.io/badge/code%20style-antiflash-FFF.svg)](https://codeberg.org/KOLANICH-tools/antiflash.py)
+
+This is a tool for patching Firefox Web Browser into allowing unsigned addons.
+
+Can be used as an apt hook.
+
+Mozilla, requiring extensions signing and signing in and getting 2FA for AMO is not nice. 🖕🔥
+
+This tool has been created as a response to
+* will of Mozilla to disallow unsigned extensions in regular builds of Firefox;
+* will of Mozilla to disallow WebExtensions Experiments in regular builds of Firefox;
+* will of Mozilla to require authentication on AMO in order to sign extensions;
+* unwillingness of devs of some distros to provide "Developer Edition" builds of Firefox.
+
+ToDo: Currently libzip is used for updating files witin the archive. It doesn't allow rewriting files in archives without creating a copy of the archive. [It is considered contradicting `libzip` goals according to its authors.](https://github.com/nih-at/libzip/issues/304) We need a lib allowing to do that.
+
+## Installation
+0. Learn how to install python packages from git.
+1. Install manually the latest versions of the dependencies mentioned in the `Dependencies` section of this ReadMe.
+2. Install this tool.
+
+## How to use
+1. Copy the original `omni.ja` to the current dir
+```bash
+cp /usr/lib/firefox/omni.ja ./omni.ja.bak
+cp ./omni.ja.bak ./omni.ja
+```
+2. Modify it with `firefucks` tool
+```bash
+firefucks ./omni.ja
+```
+3. Copy it back
+```bash
+sudo fakeroot cp ./omni.ja /usr/lib/firefox/omni.ja
+```
+4. **IMPORTANT, without this the changes will have no effect!** (ToDo: figure out what is the internal mechanism invalidating the caches, and maybe the way to patch the data within caches without needing root). Clean the startup caches:
+```bash
+rm -rf ~/.cache/mozilla/firefox/*/startupCache
+```
+
+## Check that it has worked
+1. Open `Tools -> Browser Tools -> Browser Console`.
+2. Paste there content of [`snippet.js`](./snippet.js) and execute it. It will print an object with the current values of the variables.
+3. Compare them against the [`preset.json` file](./firefucks/preset.json) shipped as a part of this tool.
+
+
+## Principle of operation
+
+Some critical browser-related code written in JS and some resources are stored in `omni.ja` files, which are zip archives. The location of these files is following:
+
+```bash
+dpkg -L firefox | grep omni.ja
+```
+
+```
+/usr/lib/firefox/browser/omni.ja
+/usr/lib/firefox/omni.ja
+```
+
+The latter of them (`/usr/lib/firefox/omni.ja`) contains:
+* Module `modules/AppConstants.jsm`, which contains some constants used to distinguish between flavours of Firefox;
+* Module `modules/addons/AddonSettings.jsm`, which contains some code, using the constants from `AppConstants` as input. Module `modules/addons/AddonConstants.jsm` [no longer exists](https://hg.mozilla.org/mozilla-central/rev/2766cd8808dd2d1d66bc4e9e9e313bbc60b9a197) because of this one.
+* `jsloader/resource/gre` is no longer present.
+
+
+Some of them are documented by the links:
+* https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html
+* https://firefox-source-docs.mozilla.org/toolkit/crashreporter/crashreporter/index.html
+* https://wiki.mozilla.org/Platform/Channel-specific_build_defines
+
+We are particulary interested in the following properties:
+* [`MOZ_REQUIRE_SIGNING`](https://searchfox.org/mozilla-central/search?q=symbol:AppConstants%23MOZ_REQUIRE_SIGNING), which is used to override the value `xpinstall.signatures.required`.
+* [`MOZ_DEV_EDITION`](https://searchfox.org/mozilla-central/search?q=symbol%3AAppConstants%23MOZ_DEV_EDITION), which is used to restrict access to some advanced features.
+* [`MOZ_TELEMETRY_REPORTING`](https://searchfox.org/mozilla-central/search?q=symbol:AppConstants%23MOZ_TELEMETRY_REPORTING) - used as an additional mean to disable telemetry.
+* [`MOZ_CRASHREPORTER`](https://searchfox.org/mozilla-central/search?q=symbol:AppConstants%23MOZ_CRASHREPORTER) - disables crash reporting.
+* [`MOZ_DATA_REPORTING`](https://searchfox.org/mozilla-central/search?q=symbol:AppConstants%23MOZ_DATA_REPORTING) - [disables initialization of data reporting system and disables recommendations](https://searchfox.org/mozilla-central/source/browser/components/preferences/privacy.js),
+
+Don't touch:
+* `MOZILLA_OFFICIAL` ([var](https://searchfox.org/mozilla-central/search?q=symbol%3AAppConstants%23MOZILLA_OFFICIAL), [macro](https://searchfox.org/mozilla-central/search?q=symbol:M_4924396bb8356f31)) - controls lots of different things. If you change it, your Firefox will fail to start.
+* `MOZ_WEBEXT_WEBIDL_ENABLED` ([var](https://searchfox.org/mozilla-central/search?q=symbol:%23MOZ_WEBEXT_WEBIDL_ENABLED), [macro](https://searchfox.org/mozilla-central/search?q=symbol:M_MOZ_WEBEXT_WEBIDL_ENABLED)) - [requires compile-time changes in C++ part](https://searchfox.org/mozilla-central/source/toolkit/components/extensions/webidl-api/ExtensionBrowser.cpp#67).
+
+## Thanks
+
+This tool stands on the shoulders of giants.
+
+### Dependencies
+
+* https://github.com/Kronuz/esprima-python - for JS parsing
+* https://github.com/ksons/jscodegen.py - for serializing JS back
+* https://github.com/nih-at/libzip + [its python bindings](https://github.com/KOLANICH-libs/libzip.py) - for replacing files in zip archives. **ToDo: replace with a lib doing in-place**
+
+### Sources of information
+
+* https://old.reddit.com/r/ReverseEngineering/comments/51bxuv/modifying_release_builds_of_firefox_to_allow/d7arltj/
+* https://github.com/zotero/zotero-standalone-build/blob/11e7c456732397d6b95b4b3a622990e50224b439/fetch_xulrunner.sh#L83-L90
+* https://github.com/SebastianSimon/firefox-omni-tweaks
+* https://github.com/xiaoxiaoflood/firefox-scripts/blob/master/installation-folder/config.js
+
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
index 0000000..efb9808
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to
diff --git a/firefucks/__init__.py b/firefucks/__init__.py
new file mode 100644
index 0000000..577b94c
--- /dev/null
+++ b/firefucks/__init__.py
@@ -0,0 +1,156 @@
+import typing
+from pathlib import Path, PurePath
+
+import esprima
+import escodegen
+from esprima.nodes import Script
+from libzip.Archive import Archive
+from libzip.enums import OpenFlags
+from libzip.Source import Source
+
+from .constants import appConstraintsInternalPath, omniJaLinuxPath
+from .json import json
+from .patcher import patchAppConstants
+
+# pylint:disable=too-few-public-methods
+
+__all__ = ("PatchingPipeline", "DEFAULT_PRESET")
+
+thisDir = Path(__file__).absolute().parent
+presetFile = thisDir / "preset.json"
+DEFAULT_PRESET = json.loads(presetFile.read_text())
+
+
+class InternalPaths:
+ __slots__ = ("appConstraints",)
+
+ def __init__(self, appConstraints: PurePath = appConstraintsInternalPath) -> None:
+ self.appConstraints = appConstraints
+
+
+class Paths:
+ __slots__ = ("root", "internal")
+
+ def __init__(self, root: Path, internal: InternalPaths = None) -> None:
+ self.root = root
+ if internal is None:
+ internal = InternalPaths()
+
+ self.internal = internal
+
+
+class PathsPair:
+ __slots__ = ("root", "internal")
+
+ def __init__(self, root: Path, internal: PurePath) -> None:
+ self.root = root
+ self.internal = internal
+
+
+class DestinationBackend:
+ __slots__ = ()
+
+ def getFileText(self, pp: PathsPair) -> str:
+ raise NotImplementedError
+
+ def writeBack(self, pp: PathsPair, source: str) -> None:
+ raise NotImplementedError
+
+ @classmethod
+ def make(cls, patchee: Paths) -> "DestinationBackend":
+ if patchee.root.is_dir():
+ return DirDestinationBackend()
+ return ArchiveDestinationBackend()
+
+
+class DirDestinationBackend(DestinationBackend):
+ __slots__ = ()
+
+ def getFileText(self, pp: PathsPair) -> str:
+ return (pp.root / pp.internal).read_text()
+
+ def writeBack(self, pp: PathsPair, source: str) -> None:
+ (pp.root / pp.internal).write_text(source)
+
+
+class ArchiveDestinationBackend(DestinationBackend):
+ __slots__ = ()
+
+ def getFileText(self, pp: PathsPair) -> str:
+ with Archive(pp.root, OpenFlags.read_only | OpenFlags.check) as a:
+ f = a[pp.internal]
+ appConstsText = bytes(f.stat.originalSize)
+ with f as of:
+ of.read(appConstsText)
+ return appConstsText.decode("utf-8")
+
+ def writeBack(self, pp: PathsPair, source: str) -> None:
+ with Archive(pp.root, OpenFlags.read_write | OpenFlags.check) as a:
+ f = a[pp.internal]
+ s = Source.make(source.encode("utf-8"))
+ f.replace(s)
+
+
+class ParsedAST:
+ __slots__ = ("ast", "pp")
+
+ def __init__(self, pp: PathsPair) -> None:
+ self.pp = pp
+ self.ast = None
+
+ def load(self, destinationBackend: DestinationBackend) -> None:
+ source = destinationBackend.getFileText(self.pp)
+ self.ast = self.parse(source)
+
+ def dump(self, destinationBackend: DestinationBackend) -> None:
+ res = self.serialize(esprima.toDict(self.ast))
+ destinationBackend.writeBack(self.pp, res)
+
+ def parse(self, source):
+ raise NotImplementedError
+
+ def serialize(self, source):
+ raise NotImplementedError
+
+
+class JSParsedAST(ParsedAST):
+ __slots__ = ()
+
+ def parse(self, source: str) -> Script:
+ return esprima.parse(source, {"comment": True})
+
+ def serialize(self, source: typing.Dict) -> str:
+ return escodegen.generate(esprima.toDict(self.ast))
+
+
+class PatchingPipeline:
+ __slots__ = ("preset", "patchee", "destinationBackend", "appConsts", "unpatchedProps")
+
+ appConstraintsInternalPath = appConstraintsInternalPath
+
+ def __init__(self, preset: typing.Dict[str, bool], patchee: typing.Union[Paths, Path], destinationBackend: typing.Optional[DestinationBackend] = None) -> None:
+ self.preset = preset
+ if not isinstance(patchee, Paths):
+ patchee = Paths(patchee)
+
+ self.patchee = patchee
+ if destinationBackend is None:
+ destinationBackend = DestinationBackend.make(patchee)
+ self.destinationBackend = destinationBackend
+ self.appConsts = JSParsedAST(PathsPair(patchee.root, patchee.internal.appConstraints))
+ self.unpatchedProps = None
+
+ def __call__(self) -> typing.Dict[typing.Any, typing.Any]:
+ self.load()
+ self.patch()
+ self.dump()
+ return self.unpatchedProps
+
+ def load(self) -> None:
+ self.appConsts.load(self.destinationBackend)
+
+ def patch(self) -> None:
+ self.unpatchedProps = patchAppConstants(self.appConsts.ast, self.preset)
+
+ def dump(self) -> None:
+ self.appConsts.dump(self.destinationBackend)
diff --git a/firefucks/__main__.py b/firefucks/__main__.py
new file mode 100644
index 0000000..472a165
--- /dev/null
+++ b/firefucks/__main__.py
@@ -0,0 +1,18 @@
+import argparse
+from pathlib import Path
+
+from . import DEFAULT_PRESET, PatchingPipeline
+from .constants import omniJaLinuxPath
+
+
+def main() -> None:
+ p = argparse.ArgumentParser(prog=None, usage=None, description="A patcher to make Firefox into allowing unsigned addons.", exit_on_error=False)
+ p.add_argument(dest="path", metavar="", default="./omni.ja", type=Path, help="The path to `" + str(omniJaLinuxPath) + "` archive or unpacked dir")
+ args = p.parse_args()
+ pl = PatchingPipeline(DEFAULT_PRESET, args.path)
+ unpatched = pl()
+ print("unpatched", unpatched)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/firefucks/constants.py b/firefucks/constants.py
new file mode 100644
index 0000000..1e28de4
--- /dev/null
+++ b/firefucks/constants.py
@@ -0,0 +1,6 @@
+from pathlib import Path, PurePath
+
+__all__ = ("omniJaLinuxPath", "appConstraintsInternalPath")
+
+omniJaLinuxPath = Path("/usr/lib/firefox/omni.ja")
+appConstraintsInternalPath = PurePath("modules/AppConstants.jsm")
diff --git a/firefucks/json.py b/firefucks/json.py
new file mode 100644
index 0000000..332ca9a
--- /dev/null
+++ b/firefucks/json.py
@@ -0,0 +1,5 @@
+# pylint:disable=unused-import
+try:
+ import mujson as json
+except ImportError:
+ import json
diff --git a/firefucks/patcher.py b/firefucks/patcher.py
new file mode 100644
index 0000000..9ed633e
--- /dev/null
+++ b/firefucks/patcher.py
@@ -0,0 +1,78 @@
+import typing
+
+import esprima
+from esprima.nodes import CallExpression, Literal, ObjectExpression, Script, StaticMemberExpression
+
+from .json import json
+
+
+def isPropParent(cand: StaticMemberExpression, pathComp: str) -> bool:
+ if pathComp == "this":
+
+ def objPred0(o):
+ return o.type == "ThisExpression"
+
+ else:
+
+ def objPred0(o):
+ return o.type == "Identifier" and o.name == pathComp
+
+ return cand.type == "MemberExpression" and objPred0(cand.object)
+
+
+def isPropChild(cand: StaticMemberExpression, pathComp: str) -> bool:
+ p = cand.property
+ return p.type == "Identifier" and p.name == pathComp
+
+
+def isProp2(cand: StaticMemberExpression, pathComp0: str, pathComp1: str) -> bool:
+ return isPropParent(cand, pathComp0) and isPropChild(cand, pathComp1)
+
+
+def findThisAssignmentPropInProgram(progNode: Script, propsToFind: set, op: str = "=") -> typing.Dict[str, CallExpression]:
+ assert progNode.type == "Program"
+ res = {}
+ for assignmentExprCand1 in progNode.body:
+ if assignmentExprCand1 and assignmentExprCand1.type == "ExpressionStatement":
+ assignmentExprCand = assignmentExprCand1.expression
+ if assignmentExprCand and assignmentExprCand.type == "AssignmentExpression" and assignmentExprCand.operator == op:
+ lhs = assignmentExprCand.left
+ if isPropParent(lhs, "this"):
+ p = lhs.property
+ if p.type == "Identifier" and p.name in propsToFind:
+ res[p.name] = assignmentExprCand.right
+ return res
+
+
+def literal2ast(v: bool) -> Literal:
+ return esprima.parse(json.dumps(v)).body[0].expression
+
+
+def patchDictExpr(dictExpr: ObjectExpression, patch: dict) -> typing.Dict[typing.Any, typing.Any]:
+ patch = type(patch)(patch)
+ for cand in dictExpr.properties:
+ if cand.key.type == "Identifier" and cand.key.name in patch:
+ try:
+ replacementVal = patch[cand.key.name]
+ except KeyError:
+ continue
+ else:
+ targetP = cand
+ targetP.value = literal2ast(replacementVal)
+ del patch[cand.key.name]
+ if not patch:
+ break
+ return patch
+
+
+def patchAppConstants(res: Script, patch: typing.Dict[str, bool]) -> typing.Dict[typing.Any, typing.Any]:
+ appConstantsExpr = findThisAssignmentPropInProgram(res, {"AppConstants"})["AppConstants"]
+ if isProp2(appConstantsExpr.callee, "Object", "freeze"):
+ aS = appConstantsExpr.arguments
+ if aS:
+ appConstantsExpr = aS[0] # unfrozen expression
+
+ if appConstantsExpr.type == "ObjectExpression":
+ return patchDictExpr(appConstantsExpr, patch)
+
+ raise ValueError("this.AppConstants is assigned with wrong thing:", appConstantsExpr)
diff --git a/firefucks/preset.json b/firefucks/preset.json
new file mode 100644
index 0000000..1570199
--- /dev/null
+++ b/firefucks/preset.json
@@ -0,0 +1,7 @@
+{
+ "MOZ_REQUIRE_SIGNING": false,
+ "MOZ_DEV_EDITION": true,
+ "MOZ_TELEMETRY_REPORTING": false,
+ "MOZ_CRASHREPORTER": false,
+ "MOZ_DATA_REPORTING": false
+}
diff --git a/firefucks/py.typed b/firefucks/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3e259e5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,43 @@
+[build-system]
+build-backend = "setuptools.build_meta"
+requires = ["setuptools>=61.2.0", "wheel", "setuptools_scm[toml]>=3.4.3"]
+
+[project]
+name = "firefucks"
+description = "A tool for patching Firefox Web Browser into allowing unsigned addons."
+readme = "ReadMe.md"
+keywords = ["firefox", "icewasel", "mozilla", "webextensions"]
+license = {text = "Unlicense"}
+authors = [{name = "KOLANICH"}]
+requires-python = ">=3.4"
+dependencies = [
+ "esprima", # @ git+https://github.com/Kronuz/esprima-python
+ #"jscodegen", # @ git+https://github.com/ksons/jscodegen.py
+ "escodegen", # @ git+https://github.com/0o120/escodegen-python
+ "libzip", # @ git+https://codeberg.org/KOLANICH-libs/libzip.py
+]
+dynamic = ["version"]
+classifiers = [
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Development Status :: 4 - Beta",
+ "Environment :: Other Environment",
+ "Intended Audience :: Developers",
+ "License :: Public Domain",
+ "Operating System :: OS Independent",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+]
+[project.urls]
+Homepage = "https://codeberg.org/KOLANICH-tools/firefucks.py"
+
+[project.scripts]
+firefucks = "firefucks.__main__:main"
+
+[tool.setuptools]
+zip-safe = true
+include-package-data = false
+
+[tool.setuptools.packages]
+find = {namespaces = false}
+
+[tool.setuptools_scm]
diff --git a/snippet.js b/snippet.js
new file mode 100644
index 0000000..dfa32e2
--- /dev/null
+++ b/snippet.js
@@ -0,0 +1,18 @@
+// Run it in browser console.
+{
+ let {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ let res = {};
+ let preset = {
+ "MOZ_REQUIRE_SIGNING": false,
+ "MOZILLA_OFFICIAL": false,
+ "MOZ_DEV_EDITION": true,
+ "MOZ_TELEMETRY_REPORTING": false,
+ "MOZ_CRASHREPORTER": false,
+ "MOZ_DATA_REPORTING": false
+ };
+ for (let [k, v] of Object.entries(preset)) {
+ //AppConstants[k] = v;
+ res[k] = AppConstants[k];
+ }
+ console.log(res);
+}
diff --git a/tests/omni.ja b/tests/omni.ja
new file mode 100644
index 0000000..8f3e87d
Binary files /dev/null and b/tests/omni.ja differ
diff --git a/tests/tests.py b/tests/tests.py
new file mode 100755
index 0000000..3c620df
--- /dev/null
+++ b/tests/tests.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+import sys
+from pathlib import Path
+import unittest
+import itertools, re
+import colorama
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from collections import OrderedDict
+
+dict = OrderedDict
+
+import firefucks
+from firefucks import *
+
+
+class Tests(unittest.TestCase):
+
+ def testSimple(self):
+ raise NotImplementedError
+
+
+if __name__ == "__main__":
+ unittest.main()