diff --git a/debian/rules b/debian/rules
index af76d81..faedc5c 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,14 +1,16 @@
#!/usr/bin/make -f
+
+### WTF ###
# See debhelper(7) (uncomment to enable)
# output every command that modifies files on the build system.
#export DH_VERBOSE = 1
-
-export PYBUILD_NAME=repolib
-export PYBUILD_OPTION=--test-pytest
-
-%:
- dh $@ --with python3 --buildsystem=pybuild
-
+#
+#export PYBUILD_NAME=repolib
+#export PYBUILD_OPTION=--test-pytest
+#
+#%:
+# dh $@ --with python3 --buildsystem=pybuild
+#
## Uncomment to disable testing during package builds
## NOTE for QA or Engineering Review: This should not be uncommented in a
## PR. If it is, DO NOT APPROVE THE PR!!!
@@ -21,4 +23,7 @@ export PYBUILD_OPTION=--test-pytest
# dh_auto_build
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bhtml docs/ build/html # HTML generator
# PYTHONPATH=. http_proxy='127.0.0.1:9' sphinx-build -N -bman docs/ build/man # Manpage generator
+### WTF ###
+%:
+ dh $@
diff --git a/main.sh b/main.sh
index 90c535f..5e24bad 100755
--- a/main.sh
+++ b/main.sh
@@ -4,16 +4,19 @@ add-apt-repository https://ppa.pika-os.com
add-apt-repository ppa:pikaos/pika
add-apt-repository ppa:kubuntu-ppa/backports
# Clone Upstream
-git clone https://github.com/pop-os/repolib
-rm -rvf ./repolib/debian
-cp -rvf ./debian ./repolib
+### WTF ###
+#git clone https://github.com/pop-os/repolib
+#rm -rvf ./repolib/debian
+### WTF ###
+cp -rvf ./python3-repolib.install ./debian/
+cp -rvf ./debian ./repolib/
cd ./repolib
# Get build deps
apt-get build-dep ./ -y
# Build package
-dpkg-buildpackage
+dpkg-buildpackage --no-sign
# Move the debs to output
cd ../
diff --git a/python3-repolib.install b/python3-repolib.install
new file mode 100644
index 0000000..197b8cc
--- /dev/null
+++ b/python3-repolib.install
@@ -0,0 +1,2 @@
+usr
+etc
diff --git a/repolib/etc/dbus-1/system.d/org.pop_os.repolib.conf b/repolib/etc/dbus-1/system.d/org.pop_os.repolib.conf
new file mode 100644
index 0000000..cadef8b
--- /dev/null
+++ b/repolib/etc/dbus-1/system.d/org.pop_os.repolib.conf
@@ -0,0 +1,20 @@
+
+
+
+ system
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/repolib/usr/bin/apt-manage b/repolib/usr/bin/apt-manage
new file mode 100755
index 0000000..77ae812
--- /dev/null
+++ b/repolib/usr/bin/apt-manage
@@ -0,0 +1,85 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2020, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+#pylint: disable=invalid-name
+# Pylint will complain about our module name not being snake_case, however this
+# is a command rather than a python module, and thus this is correct anyway.
+
+import argparse
+import logging
+import os
+import sys
+
+import repolib
+from repolib import command
+
+SOURCES_DIR = '/etc/apt/sources.list.d'
+
+def main(options=None):
+ """ Main function for apt-manage."""
+ # Set up Argument Parsing.
+ parser = repolib.command.parser
+
+ # Parse options
+ args = parser.parse_args()
+ if options:
+ args = parser.parse_args(options)
+
+ if not args.debug:
+ args.debug = 0
+
+ if args.debug > 2:
+ args.debug = 2
+
+ verbosity = {
+ 0 : logging.WARN,
+ 1 : logging.INFO,
+ 2 : logging.DEBUG
+ }
+
+ log = logging.getLogger('apt-manage')
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
+ handler.setFormatter(formatter)
+ log.addHandler(handler)
+ log.setLevel(verbosity[args.debug])
+ log.debug('Logging set up!')
+ repolib.set_logging_level(args.debug)
+
+ if not args.action:
+ args = parser.parse_args(sys.argv[1:] + ['list'])
+
+ log.debug('Arguments passed: %s', str(args))
+ log.debug('Got command: %s', args.action)
+
+ subcommand = args.action.capitalize()
+
+ command = getattr(repolib.command, subcommand)(log, args, parser)
+ result = command.run()
+ if not result:
+ sys.exit(1)
+
+if __name__ == '__main__':
+ try:
+ main()
+ except KeyboardInterrupt:
+ print('')
+ sys.exit(130)
diff --git a/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/PKG-INFO b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/PKG-INFO
new file mode 100644
index 0000000..d183cd9
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/PKG-INFO
@@ -0,0 +1,182 @@
+Metadata-Version: 2.1
+Name: repolib
+Version: 2.2.0
+Summary: Easily manage software sources
+Home-page: https://github.com/pop-os/repolib
+Author: Ian Santopietro
+Author-email: ian@system76.com
+License: LGPLv3
+Download-URL: https://github.com/pop-os/repolib/releases
+Platform: UNKNOWN
+License-File: LICENSE
+License-File: LICENSE.LESSER
+
+=======
+RepoLib
+=======
+
+RepoLib is a Python library and CLI tool-set for managing your software
+system software repositories. It's currently set up to handle APT repositories
+on Debian-based linux distributions.
+
+RepoLib is intended to operate on DEB822-format sources. It aims to provide
+feature parity with software-properties for most commonly used functions.
+
+Documentation
+=============
+
+Documentation is available online at `Read The Docs `_.
+
+
+Basic CLI Usage
+---------------
+
+RepoLib includes a CLI program for managing software repositories,
+:code:`apt-manage`
+.
+
+Usage is divided into subcommands for most tasks. Currently implemented commands
+are:
+
+ apt-manage add # Adds repositories to the system
+ apt-manage list # Lists configuration details of repositories
+
+Additional information is available with the built-in help:
+
+ apt-manage --help
+
+
+Add
+^^^
+
+Apt-manage allows entering a URL for a repository, a complete debian line, or a
+Launchpad PPA shortcut (e.g. "ppa:user/repo"). It also adds signing keys for PPA
+style repositories automatically.
+
+
+List
+^^^^
+
+With no options, it outputs a list of the currently configured repositories on
+the system (all those found in
+:code:`/etc/apt/sources.list.d/`
+. With a configured repository as an argument, it outputs the configuration
+details of the specified repository.
+
+Remove
+^^^^^^
+
+Accepts one repository as an argument. Removes the specified repository.
+
+NOTE: The system repository (/etc/at/sources.list.d/system.sources) cannot be
+removed.
+
+Source
+^^^^^^
+
+Allows enabling or disabling source code for the given repository.
+
+Modify
+^^^^^^
+
+Allows changing configuration details of a given repository
+
+Installation
+============
+
+From System Package Manager
+---------------------------
+
+If your operating system packages repolib, you can install it by running::
+
+ sudo apt install python3-repolib
+
+
+Uninstall
+^^^^^^^^^
+
+To uninstall, simply do::
+
+ sudo apt remove python3-repolib
+
+
+From PyPI
+---------
+
+Repolib is available on PyPI. You can install it for your current user with::
+
+ pip3 install repolib
+
+Alternatively, you can install it system-wide using::
+
+ sudo pip3 install repolib
+
+Uninstall
+^^^^^^^^^
+
+To uninstall, simply do::
+
+ sudo pip3 uninstall repolib
+
+From Git
+--------
+
+First, clone the git repository onto your local system::
+
+ git clone https://github.com/isantop/repolib
+ cd repolib
+
+Debian
+------
+
+On debian based distributions, you can build a .deb package locally and install
+it onto your system. You will need the following build-dependencies:
+
+ * debhelper (>=11)
+ * dh-python
+ * python3-all
+ * python3-setuptools
+
+You can use this command to install these all in one go::
+
+ sudo apt install debhelper dh-python python3-all python3-setuptools
+
+Then build and install the package::
+
+ debuild -us -uc
+ cd ..
+ sudo dpkg -i python3-repolib_*.deb
+
+Uninstall
+^^^^^^^^^
+
+To uninstall, simply do::
+
+ sudo apt remove python3-repolib
+
+setuptools setup.py
+-------------------
+
+You can build and install the package using python3-setuptools. First, install
+the dependencies::
+
+ sudo apt install python3-all python3-setuptools
+
+Then build and install the package::
+
+ sudo python3 ./setup.py install
+
+Uninstall
+^^^^^^^^^
+
+You can uninstall RepoLib by removing the following files/directories:
+
+ * /usr/local/lib/python3.7/dist-packages/repolib/
+ * /usr/local/lib/python3.7/dist-packages/repolib-\*.egg-info
+ * /usr/local/bin/apt-manage
+
+This command will remove all of these for you::
+
+ sudo rm -r /usr/local/lib/python3.7/dist-packages/repolib* /usr/local/bin/apt-manage
+
+
diff --git a/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/dependency_links.txt b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/requires.txt b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/requires.txt
new file mode 100644
index 0000000..a8ef404
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/requires.txt
@@ -0,0 +1 @@
+gnupg
diff --git a/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/top_level.txt b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/top_level.txt
new file mode 100644
index 0000000..d4b84bf
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib-2.2.0.egg-info/top_level.txt
@@ -0,0 +1,4 @@
+repolib
+repolib/command
+repolib/shortcuts
+repolib/unittest
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/__init__.py b/repolib/usr/lib/python3/dist-packages/repolib/__init__.py
new file mode 100644
index 0000000..4b4b7e5
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/__init__.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+import logging.handlers as handlers
+
+from pathlib import Path
+
+from . import __version__
+
+VERSION = __version__.__version__
+
+from .file import SourceFile, SourceFileError
+from .source import Source, SourceError
+from .shortcuts import PPASource, PopdevSource, shortcut_prefixes
+from .key import SourceKey, KeyFileError
+from . import util
+from . import system
+
+LOG_FILE_PATH = '/var/log/repolib.log'
+LOG_LEVEL = logging.WARNING
+KEYS_DIR = util.KEYS_DIR
+SOURCES_DIR = util.SOURCES_DIR
+TESTING = util.TESTING
+KEYSERVER_QUERY_URL = util.KEYSERVER_QUERY_URL
+DISTRO_CODENAME = util.DISTRO_CODENAME
+PRETTY_PRINT = util.PRETTY_PRINT
+CLEAN_CHARS = util.CLEAN_CHARS
+
+try:
+ from systemd.journal import JournalHandler
+ systemd_support = True
+except ImportError:
+ systemd_support = False
+
+## Setup logging
+stream_fmt = logging.Formatter(
+ '%(name)-21s: %(levelname)-8s %(message)s'
+)
+file_fmt = logging.Formatter(
+ '%(asctime)s - %(name)-21s: %(levelname)-8s %(message)s'
+)
+log = logging.getLogger(__name__)
+
+console_log = logging.StreamHandler()
+console_log.setFormatter(stream_fmt)
+console_log.setLevel(LOG_LEVEL)
+
+# file_log = handlers.RotatingFileHandler(
+# LOG_FILE_PATH, maxBytes=(1048576*5), backupCount=5
+# )
+# file_log.setFormatter(file_fmt)
+# file_log.setLevel(LOG_LEVEL)
+
+log.addHandler(console_log)
+# log.addHandler(file_log)
+
+log_level_map:dict = {
+ 0: logging.WARNING,
+ 1: logging.INFO,
+ 2: logging.DEBUG
+}
+
+if systemd_support:
+ journald_log = JournalHandler() # type: ignore (this is handled by the wrapping if)
+ journald_log.setLevel(logging.INFO)
+ journald_log.setFormatter(stream_fmt)
+ log.addHandler(journald_log)
+
+log.setLevel(logging.DEBUG)
+
+def set_testing(testing:bool = True) -> None:
+ """Puts Repolib into testing mode"""
+ global KEYS_DIR
+ global SOURCES_DIR
+
+ util.set_testing(testing=testing)
+ KEYS_DIR = util.KEYS_DIR
+ SOURCES_DIR = util.SOURCES_DIR
+
+
+def set_logging_level(level:int) -> None:
+ """Set the logging level for this current repolib
+
+ Accepts an integer between 0 and 2, with 0 being the default loglevel of
+ logging.WARNING, 1 being logging.INFO, and 2 being logging.DEBUG.
+
+ Values greater than 2 are clamped to 2. Values less than 0 are clamped to 0.
+
+ Note: This only affects console output. Log file output remains
+ at logging.INFO
+
+ Arguments:
+ level(int): A logging level from 0-2
+ """
+ if level > 2:
+ level = 2
+ if level < 0:
+ level = 0
+ LOG_LEVEL = log_level_map[level]
+ console_log.setLevel(LOG_LEVEL)
+
+RepoError = util.RepoError
+SourceFormat = util.SourceFormat
+SourceType = util.SourceType
+AptSourceEnabled = util.AptSourceEnabled
+
+scrub_filename = util.scrub_filename
+url_validator = util.url_validator
+prettyprint_enable = util.prettyprint_enable
+validate_debline = util.validate_debline
+strip_hashes = util.strip_hashes
+compare_sources = util.compare_sources
+combine_sources = util.combine_sources
+sources = util.sources
+files = util.files
+keys = util.keys
+errors = util.errors
+
+valid_keys = util.valid_keys
+options_inmap = util.options_inmap
+options_outmap = util.options_outmap
+true_values = util.true_values
+
+load_all_sources = system.load_all_sources
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/__version__.py b/repolib/usr/lib/python3/dist-packages/repolib/__version__.py
new file mode 100644
index 0000000..a56edd5
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/__version__.py
@@ -0,0 +1,22 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+__version__ = "2.2.0"
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/__init__.py b/repolib/usr/lib/python3/dist-packages/repolib/command/__init__.py
new file mode 100644
index 0000000..86f8e6c
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/__init__.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from .argparser import get_argparser
+from .add import Add
+from .list import List
+from .remove import Remove
+from .modify import Modify
+from .key import Key
+
+parser = get_argparser()
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/add.py b/repolib/usr/lib/python3/dist-packages/repolib/command/add.py
new file mode 100644
index 0000000..5ba3f14
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/add.py
@@ -0,0 +1,267 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from httplib2.error import ServerNotFoundError
+from urllib.error import URLError
+
+from .. import util
+from ..source import Source, SourceError
+from ..file import SourceFile, SourceFileError
+from ..shortcuts import ppa, popdev, shortcut_prefixes
+from .command import Command, RepolibCommandError
+
+class Add(Command):
+ """Add subcommand
+
+ Adds a new source into the system. Requests root, if not present.
+
+ Options:
+ --disable, d
+ --source-code, -s
+ --terse, -t
+ --name, -n
+ --identifier, -i
+ --format, -f
+ --skip-keys, -k
+ """
+
+ @classmethod
+ def init_options(cls, subparsers):
+ """ Sets up the argument parser for this command.
+
+ Returns: argparse.subparser
+ The subparser for this command
+ """
+
+ sub = subparsers.add_parser(
+ 'add',
+ help='Add a new repository to the system'
+ )
+
+ sub.add_argument(
+ 'deb_line',
+ nargs='*',
+ default=['822styledeb'],
+ help='The deb line of the repository to add'
+ )
+ sub.add_argument(
+ '-d',
+ '--disable',
+ action='store_true',
+ help='Add the repository and then set it to disabled.'
+ )
+ sub.add_argument(
+ '-s',
+ '--source-code',
+ action='store_true',
+ help='Also enable source code packages for the repository.'
+ )
+ sub.add_argument(
+ '-t',
+ '--terse',
+ action='store_true',
+ help='Do not display expanded info about a repository before adding it.'
+ )
+ sub.add_argument(
+ '-n',
+ '--name',
+ default='',
+ help='A name to set for the new repo'
+ )
+ sub.add_argument(
+ '-i',
+ '--identifier',
+ default='',
+ help='The filename to use for the new source'
+ )
+ sub.add_argument(
+ '-f',
+ '--format',
+ default='sources',
+ help='The source format to save as, `sources` or `list`'
+ )
+ sub.add_argument(
+ '-k',
+ '--skip-keys',
+ action='store_true',
+ help='Skip adding signing keys (not recommended!)'
+ )
+
+ return sub
+
+ def finalize_options(self, args):
+ super().finalize_options(args)
+ self.deb_line = ' '.join(args.deb_line)
+ self.terse = args.terse
+ self.source_code = args.source_code
+ self.disable = args.disable
+ self.log.debug(args)
+
+ try:
+ name = args.name.split()
+ except AttributeError:
+ name = args.name
+
+ try:
+ ident = args.identifier.split()
+ except AttributeError:
+ ident = args.identifier
+
+ self.name = ' '.join(name)
+ pre_ident:str = '-'.join(ident)
+ self.ident = util.scrub_filename(pre_ident)
+ self.skip_keys = args.skip_keys
+ self.format = args.format.lower()
+
+ def run(self) -> bool:
+ """Run the command, return `True` if successful; otherwise `False`"""
+ if self.deb_line == '822styledeb':
+ self.parser.print_usage()
+ self.log.error('A repository is required')
+ return False
+
+ repo_is_url = self.deb_line.startswith('http')
+ repo_is_nospaced = len(self.deb_line.split()) == 1
+
+ if repo_is_url and repo_is_nospaced:
+ self.deb_line = f'deb {self.deb_line} {util.DISTRO_CODENAME} main'
+
+ print('Fetching repository information...')
+
+ self.log.debug('Adding line %s', self.deb_line)
+
+ new_source: Source = Source()
+
+ for prefix in shortcut_prefixes:
+ self.log.debug('Trying prefix %s', prefix)
+ if self.deb_line.startswith(prefix):
+ self.log.debug('Line is prefix: %s', prefix)
+ new_source = shortcut_prefixes[prefix]()
+ if not new_source.validator(self.deb_line):
+ self.log.error(
+ 'The shortcut "%s" is malformed',
+ self.deb_line
+ )
+
+ # Try and get a suggested correction for common errors
+ try:
+ if self.deb_line[len(prefix)] != ':':
+ fixed_debline: str = self.deb_line.replace(
+ self.deb_line[len(prefix)],
+ ":",
+ 1
+ )
+ print(f'Did you mean "{fixed_debline}"?')
+ except IndexError:
+ pass
+ return False
+
+ try:
+ new_source.load_from_data([self.deb_line])
+ except (URLError, ServerNotFoundError) as err:
+ import traceback
+ self.log.debug(
+ 'Exception info: %s \n %s \n %s',
+ type(err),
+ ''.join(traceback.format_exception(err)),
+ err.args
+ )
+ self.log.error(
+ 'System is offline. A connection is required to add '
+ 'PPA and Popdev sources.'
+ )
+ return False
+ except Exception as err:
+ import traceback
+ self.log.debug(
+ 'Exception info: %s \n %s \n %s',
+ type(err),
+ err.__traceback__,
+ err
+ )
+ self.log.error('An error ocurred: %s', err)
+ return False
+ break
+
+
+ if not new_source:
+ self.log.error(
+ f'Could not parse line "{self.deb_line}". Double-check the '
+ 'spelling.'
+ )
+ valid_shortcuts: str = ''
+ for shortcut in shortcut_prefixes:
+ if shortcut.startswith('deb'):
+ continue
+ valid_shortcuts += f'{shortcut}, '
+ valid_shortcuts = valid_shortcuts.strip(', ')
+ print(f'Supported repository shortcuts:\n {valid_shortcuts}')
+ return False
+
+ new_source.twin_source = True
+ new_source.sourcecode_enabled = self.source_code
+
+ if self.name:
+ new_source.name = self.name
+ if self.ident:
+ new_source.ident = self.ident
+ if self.disable:
+ new_source.enabled = False
+
+ if not new_source.ident:
+ new_source.ident = new_source.generate_default_ident()
+
+ new_file = SourceFile(name=new_source.ident)
+ new_file.format = new_source.default_format
+ if self.format:
+ self.log.info('Setting new source format to %s', self.format)
+ for format in util.SourceFormat:
+ if self.format == format.value:
+ new_file.format = format
+ self.log.debug('New source format set to %s', format.value)
+
+ new_file.add_source(new_source)
+ new_source.file = new_file
+ self.log.debug('File format: %s', new_file.format)
+ self.log.debug('File path: %s', new_file.path)
+
+ self.log.debug('Sources in file %s:\n%s', new_file.path, new_file.sources)
+
+ if not self.terse:
+ print(
+ 'Adding the following source: \n',
+ new_source.get_description(),
+ '\n\n',
+ new_source.ui
+ )
+ try:
+ input(
+ 'Press ENTER to continue adding this source or Ctrl+C '
+ 'to cancel'
+ )
+ except KeyboardInterrupt:
+ # Handled here to avoid printing the exception to console
+ exit(0)
+
+ new_file.save()
+ util.dbus_quit()
+ return True
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/argparser.py b/repolib/usr/lib/python3/dist-packages/repolib/command/argparser.py
new file mode 100644
index 0000000..182ccd7
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/argparser.py
@@ -0,0 +1,65 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2020, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+
+Module for getting an argparser. Used by apt-manage
+"""
+
+import argparse
+import inspect
+
+from repolib import command as cmd
+
+from .. import __version__
+
+def get_argparser():
+ """ Get an argument parser with our arguments.
+
+ Returns:
+ An argparse.ArgumentParser.
+ """
+ parser = argparse.ArgumentParser(
+ prog='apt-manage',
+ description='Manage software sources and repositories',
+ epilog='apt-manage version: {}'.format(__version__.__version__)
+ )
+
+ parser.add_argument(
+ '-b',
+ '--debug',
+ action='count',
+ help=argparse.SUPPRESS
+ )
+
+ subparsers = parser.add_subparsers(
+ help='...',
+ dest='action',
+ metavar='COMMAND'
+ )
+
+ subcommands = []
+ for i in inspect.getmembers(cmd, inspect.isclass):
+ obj = getattr(cmd, i[0])
+ subcommands.append(obj)
+
+ for i in subcommands:
+ i.init_options(subparsers)
+
+ return parser
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/command.py b/repolib/usr/lib/python3/dist-packages/repolib/command/command.py
new file mode 100644
index 0000000..b4afa41
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/command.py
@@ -0,0 +1,77 @@
+"""
+Copyright (c) 2020, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+
+Command line application helper class.
+"""
+
+from ..util import SOURCES_DIR
+
+class RepolibCommandError(Exception):
+ """ Exceptions generated by Repolib CLIs."""
+
+ def __init__(self, *args, code=1, **kwargs):
+ """Exception from a CLI
+
+ Arguments:
+ code (:obj:`int`, optional, default=1): Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code = code
+
+class Command:
+ # pylint: disable=no-self-use,too-few-public-methods
+ # This is a base class for other things to inherit and give other programs
+ # a standardized interface for interacting with commands.
+ """ CLI helper class for developing command line applications."""
+
+ def __init__(self, log, args, parser):
+ self.log = log
+ self.parser = parser
+ self.sources_dir = SOURCES_DIR
+ self.finalize_options(args)
+
+ def finalize_options(self, args):
+ """ Base options parsing class.
+
+ Use this class in commands to set up the final options parsing and set
+ instance variables for later use.
+ """
+ self.verbose = False
+ self.debug = False
+ if args.debug:
+ if args.debug > 1:
+ self.verbose = True
+ if args.debug != 0:
+ self.log.info('Debug mode set, not-saving any changes.')
+ self.debug = True
+
+ def run(self):
+ """ The initial base for running the command.
+
+ This needs to have a standardized format, argument list, and return
+ either True or False depending on whether the command was successful or
+ not.
+
+ Returns:
+ True if the command succeeded, otherwise False.
+ """
+
+ # Since this is just the base, it should always return True (because
+ # you shouldn't fail at doing nothing).
+ return True
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/key.py b/repolib/usr/lib/python3/dist-packages/repolib/command/key.py
new file mode 100644
index 0000000..ccd97d1
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/key.py
@@ -0,0 +1,375 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from pathlib import Path
+
+from ..key import SourceKey
+from .. import system, util
+
+from .command import Command
+
+KEYS_PATH = Path(util.KEYS_DIR)
+
+class Key(Command):
+ """Key subcommand.
+
+ Manages signing keys for repositories, allowing adding, removal, and
+ fetching of remote keys.
+
+ Options:
+ --name, -n
+ --path, -p
+ --url, -u
+ --ascii, -a
+ --fingerprint, -f
+ --keyserver, -s
+ --remove, -r
+ """
+
+ @classmethod
+ def init_options(cls, subparsers) -> None:
+ """Sets up this command's options parser"""
+
+ sub = subparsers.add_parser(
+ 'key',
+ help='Manage repository signing keys',
+ epilog=(
+ 'Note that no verification of key validity is performed when '
+ 'managing key using apt-mange. Ensure that keys are valid to '
+ 'avoid errors with updates.'
+ )
+ )
+
+ sub.add_argument(
+ 'repository',
+ default=['x-repolib-none'],
+ help='Which repository to manage keys for.'
+ )
+
+ sub_group = sub.add_mutually_exclusive_group()
+
+ sub_group.add_argument(
+ '-n',
+ '--name',
+ help='The name of an existing key file to set.'
+ )
+
+ sub_group.add_argument(
+ '-p',
+ '--path',
+ help='Sets a path on disk to a signing key.'
+ )
+
+ sub_group.add_argument(
+ '-u',
+ '--url',
+ help='Download a key over HTTPS'
+ )
+
+ sub_group.add_argument(
+ '-a',
+ '--ascii',
+ help='Add an ascii-armored key'
+ )
+
+ sub_group.add_argument(
+ '-f',
+ '--fingerprint',
+ help=(
+ 'Fetch a key via fingerprint from a keyserver. Use --keyserver '
+ 'to specify the URL to the keyserver.'
+ )
+ )
+
+ sub_group.add_argument(
+ '-i',
+ '--info',
+ action='store_true',
+ help='Print key information'
+ )
+
+ sub_group.add_argument(
+ '-r',
+ '--remove',
+ action='store_true',
+ help=(
+ 'Remove a signing key from the repo. If no other sources use '
+ 'this key, its file will be deleted.'
+ )
+ )
+
+ sub.add_argument(
+ '-s',
+ '--keyserver',
+ help=(
+ 'The keyserver from which to fetch the given fingerprint. '
+ '(Default: keyserver.ubuntu.com)'
+ )
+ )
+
+ def finalize_options(self, args):
+ super().finalize_options(args)
+ self.repo = args.repository
+ self.keyserver = args.keyserver
+
+ self.actions:dict = {}
+ self.system_source = False
+
+ for act in [
+ 'name',
+ 'path',
+ 'url',
+ 'ascii',
+ 'fingerprint',
+ 'info',
+ 'remove',
+ ]:
+ self.actions[act] = getattr(args, act)
+
+ system.load_all_sources()
+
+ def run(self):
+ """Run the command"""
+ self.log.info('Modifying signing key settings for %s', self.repo)
+
+ if not self.repo:
+ self.log.error('No repository provided')
+ return False
+
+ try:
+ self.source = util.sources[self.repo]
+ if self.repo == 'system':
+ self.system_source = True
+ except KeyError:
+ self.log.error(
+ 'The repository %s was not found. Check the spelling',
+ self.repo
+ )
+ return False
+
+ self.log.debug('Actions to take:\n%s', self.actions)
+ # Run info, unless a different action is specified
+ self.actions['info'] = True
+ for key in self.actions:
+ if key == 'info':
+ self.log.debug('Skipping info key')
+ continue
+ if self.actions[key]:
+ self.log.info('Got an action, skipping info')
+ self.actions['info'] = False
+ break
+
+ self.log.debug('Source before:\n%s', self.source)
+
+ rets = []
+ for action in self.actions:
+ if self.actions[action]:
+ self.log.debug('Running action: %s - (%s)', action, self.actions[action])
+ ret = getattr(self, action)(self.actions[action])
+ rets.append(ret)
+ break
+
+ self.log.debug('Results: %s', rets)
+ self.log.debug('Source after: \n%s', self.source)
+
+ if self.actions['info']:
+ self.log.info('Running Info, skipping saving %s', self.source.ident)
+ return True
+
+ if True in rets:
+ self.log.info('Saving source %s', self.source.ident)
+ self.source.file.save()
+ return True
+ else:
+ self.log.warning('No valid changes specified, no actions taken.')
+ return False
+
+ def name(self, value:str) -> bool:
+ """Sets the key file to a name in the key file directory"""
+ if not value:
+ return False
+
+ key_path = None
+
+ for ext in ['.gpg', '.asc']:
+ if value.endswith(ext):
+ path = KEYS_PATH / value
+ if path.exists():
+ key_path = path
+ break
+ else:
+ if 'archive-keyring' not in value:
+ value += '-archive-keyring'
+ path = KEYS_PATH / f'{value}{ext}'
+ if path.exists():
+ key_path = path
+ break
+
+ if not key_path:
+ self.log.error('The key file %s could not be found', value)
+ return False
+
+ return self.path(str(key_path))
+
+ def path(self, value:str) -> bool:
+ """Sets the key file to the given path"""
+ if not value:
+ return False
+
+ key_path = Path(value)
+
+ if not key_path.exists():
+ self.log.error(
+ 'The path %s does not exist', value
+ )
+ return False
+
+ self.source.signed_by = str(key_path)
+ self.source.load_key()
+ return True
+
+ def url(self, value:str) -> bool:
+ """Downloads a key to use from a provided URL."""
+ if not value:
+ return False
+
+ if not value.startswith('https:'):
+ response = 'n'
+ self.log.warning(
+ 'The key is not being downloaded over an encrypted connection, '
+ 'and may be tampered with in-transit. Only add keys that you '
+ 'trust, from sources which you trust.'
+ )
+ response = input('Do you want to continue? (y/N): ')
+ if response not in util.true_values:
+ return False
+
+ key = SourceKey(name=self.source.ident)
+ key.load_key_data(url=value)
+ self.source.key = key
+ self.source.signed_by = str(key.path)
+ self.source.load_key()
+ return True
+
+ def ascii(self, value:str) -> bool:
+ """Loads the key from provided ASCII-armored data"""
+ if not value:
+ return False
+
+ response = 'n'
+ print('Only add signing keys from data you trust.')
+ response = input('Do you want to continue? (y/N): ')
+
+ if response not in util.true_values:
+ return False
+
+ key = SourceKey(name=self.source.ident)
+ key.load_key_data(ascii=value)
+ self.source.key = key
+ self.source.signed_by = str(key.path)
+ self.source.load_key()
+ return True
+
+ def fingerprint(self, value:str) -> bool:
+ """Downloads the key with the given fingerprint from a keyserver"""
+ if not value:
+ return False
+
+ key = SourceKey(name=self.source.ident)
+
+ if self.keyserver:
+ key.load_key_data(fingerprint=value, keyserver=self.keyserver)
+ else:
+ key.load_key_data(fingerprint=value)
+
+ self.source.key = key
+ self.source.signed_by = str(key.path)
+ self.source.load_key()
+ return True
+
+ def info(self, value:str) -> bool:
+ """Prints key information"""
+ from datetime import date
+ if not self.source.key:
+ self.log.error(
+ 'The source %s does not have a key configured.',
+ self.repo
+ )
+ return True
+
+ else:
+ key:dict = self.source.get_key_info()
+ output: str = f'Key information for source {self.source.ident}:\n'
+ output += f'Key ID: {key["uids"][0]}\n'
+ output += f'Fingerprint: {key["keyid"]}\n'
+ if key['type'] == 'pub':
+ output += 'Key Type: Public\n'
+ else:
+ output += 'Key Type: Private\n'
+ keydate = date.fromtimestamp(int(key['date']))
+ output += f'Issue Date: {keydate.ctime()}\n'
+ output += f'Length: {key["length"]} Bytes\n'
+ output += f'Keyring Path: {str(self.source.key.path)}\n'
+ print(output)
+
+ return True
+
+ def remove(self, value:str) -> bool:
+ """Removes the key from the source"""
+
+ if not self.source.key:
+ self.log.error(
+ 'The source %s does not have a key configured.',
+ self.repo
+ )
+ return False
+
+ response = 'n'
+ print(
+ 'If you remove the key, there may no longer be any verification '
+ 'of software packages from this repository, including for future '
+ 'updates. This may cause errors with your updates.'
+ )
+ response = input('Do you want to continue? (y/N): ')
+ if response not in util.true_values:
+ return False
+
+ old_key = self.source.key
+ self.source.key = None
+ self.source.signed_by = ''
+
+ for source in util.sources.values():
+ if source.key == old_key:
+ self.log.info(
+ 'Key file %s in use with another key, not deleting',
+ old_key.path
+ )
+ return True
+
+ response = 'n'
+ print('No other sources were found which use this key.')
+ response = input('Do you want to remove it? (Y/n): ')
+ if response in util.true_values:
+ old_key.delete_key()
+
+ return True
+
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/list.py b/repolib/usr/lib/python3/dist-packages/repolib/command/list.py
new file mode 100644
index 0000000..aad121b
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/list.py
@@ -0,0 +1,216 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import argparse
+import textwrap
+import traceback
+
+from ..file import SourceFile, SourceFileError
+from ..source import Source, SourceError
+from .. import RepoError, util, system
+
+from .command import Command, RepolibCommandError
+
+class List(Command):
+ """List subcommand
+
+ Lists information about currently-configured sources on the system.
+
+ Options:
+ --legacy, -l
+ --verbose, -v
+ --all, -a
+ --no-names, -n
+ --file-names, -f
+ --no-indentation
+ """
+
+ @classmethod
+ def init_options(cls, subparsers):
+ """Sets up ths argument parser for this command.
+
+ Returns: argparse.subparser:
+ This command's subparser
+ """
+
+ sub = subparsers.add_parser(
+ 'list',
+ help=(
+ 'List information for configured repostiories. If a repository '
+ 'name is provided, details about that repository are printed.'
+ )
+ )
+
+ sub.add_argument(
+ 'repository',
+ nargs='*',
+ default=['x-repolib-all-sources'],
+ help='The repository for which to list configuration'
+ )
+ sub.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='Show additional information, if available.'
+ )
+ sub.add_argument(
+ '-a',
+ '--all',
+ action='store_true',
+ help='Display full configuration for all configured repositories.'
+ )
+ sub.add_argument(
+ '-l',
+ '--legacy',
+ action='store_true',
+ help='Include repositories configured in legacy sources.list file.'
+ )
+ sub.add_argument(
+ '-n',
+ '--no-names',
+ action='store_true',
+ dest='skip_names',
+ help=argparse.SUPPRESS
+ )
+ sub.add_argument(
+ '-f',
+ '--file-names',
+ action='store_true',
+ dest='print_files',
+ help="Don't print names of files"
+ )
+ sub.add_argument(
+ '--no-indentation',
+ action='store_true',
+ dest='no_indent',
+ help=argparse.SUPPRESS
+ )
+
+ def finalize_options(self, args):
+ super().finalize_options(args)
+ self.repo = ' '.join(args.repository)
+ self.verbose = args.verbose
+ self.all = args.all
+ self.legacy = args.legacy
+ self.skip_names = args.skip_names
+ self.print_files = args.print_files
+ self.no_indent = args.no_indent
+
+ def list_legacy(self, indent) -> None:
+ """List the contents of the sources.list file.
+
+ Arguments:
+ list_file(Path): The sources.list file to try and parse.
+ indent(str): An indentation to append to the output
+ """
+ try:
+ sources_list_file = util.SOURCES_DIR.parent / 'sources.list'
+ except FileNotFoundError:
+ sources_list_file = None
+
+ print('Legacy source.list sources:')
+ if sources_list_file:
+ with open(sources_list_file, mode='r') as file:
+ for line in file:
+ if 'cdrom' in line:
+ line = ''
+
+ try:
+ source = Source()
+ source.load_from_data([line])
+ print(textwrap.indent(source.ui, indent))
+ except SourceError:
+ pass
+
+ def list_all(self):
+ """List all sources presently configured in the system
+
+ This may include the configuration data for each source as well
+ """
+
+ indent = ' '
+ if self.no_indent:
+ indent = ''
+
+ if self.print_files:
+ print('Configured source files:')
+
+ for file in util.files:
+ print(f'{file.path.name}:')
+
+ for source in file.sources:
+ print(textwrap.indent(source.ui, indent))
+
+ if self.legacy:
+ self.list_legacy(indent)
+
+ else:
+ print('Configured Sources:')
+ for source in util.sources:
+ output = util.sources[source]
+ print(textwrap.indent(output.ui, indent))
+
+ if self.legacy:
+ self.list_legacy(indent)
+
+ if util.errors:
+ print('\n\nThe following files contain formatting errors:')
+ for err in util.errors:
+ print(err)
+ if self.verbose or self.debug:
+ print('\nDetails about failing files:')
+ for err in util.errors:
+ print(f'{err}: {util.errors[err].args[0]}')
+
+ return True
+
+ def run(self):
+ """Run the command"""
+ system.load_all_sources()
+ self.log.debug("Current sources: %s", util.sources)
+ ret = False
+
+ if self.all:
+ return self.list_all()
+
+ if self.repo == 'x-repolib-all-sources' and not self.all:
+ if not self.skip_names:
+ print('Configured Sources:')
+ for source in util.sources:
+ line = source
+ if not self.skip_names:
+ line += f' - {util.sources[source].name}'
+ print(line)
+
+ return True
+
+ else:
+ try:
+ output = util.sources[self.repo]
+ print(f'Details for source {output.ui}')
+ return True
+ except KeyError:
+ self.log.error(
+ "Couldn't find the source file for %s, check the spelling",
+ self.repo
+ )
+ return False
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/modify.py b/repolib/usr/lib/python3/dist-packages/repolib/command/modify.py
new file mode 100644
index 0000000..2738684
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/modify.py
@@ -0,0 +1,502 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from argparse import SUPPRESS
+
+from .command import Command, RepolibCommandError
+from .. import util, system
+
+class Modify(Command):
+ """Modify Subcommand
+
+ Makes modifications to the specified repository
+
+ Options:
+ --enable, -e
+ --disable, -d
+ --default-mirror
+ --name
+ --add-suite
+ --remove-suite
+ --add-component
+ --remove-component
+ --add-uri
+ --remove-uri
+
+ Hidden Options
+ --add-option
+ --remove-option
+ --default-mirror
+ """
+
+ @classmethod
+ def init_options(cls, subparsers):
+ """ Sets up this command's options parser"""
+
+ sub = subparsers.add_parser(
+ 'modify',
+ help='Change a configured repository.'
+ )
+ sub.add_argument(
+ 'repository',
+ nargs='*',
+ default=['system'],
+ help='The repository to modify. Default is the system repository.'
+ )
+
+ modify_enable = sub.add_mutually_exclusive_group(
+ required=False
+ )
+ modify_enable.add_argument(
+ '-e',
+ '--enable',
+ action='store_true',
+ help='Enable the repository, if disabled.'
+ )
+ modify_enable.add_argument(
+ '-d',
+ '--disable',
+ action='store_true',
+ help=(
+ 'Disable the repository, if enabled. The system repository cannot '
+ 'be disabled.'
+ )
+ )
+
+ modify_source = sub.add_mutually_exclusive_group(
+ required=False
+ )
+
+ modify_source.add_argument(
+ '--source-enable',
+ action='store_true',
+ help='Enable source code for the repository, if disabled.'
+ )
+ modify_source.add_argument(
+ '--source-disable',
+ action='store_true',
+ help='Disable source code for the repository, if enabled.'
+ )
+
+ sub.add_argument(
+ '--default-mirror',
+ help=SUPPRESS
+ #help='Sets the default mirror for the system source.'
+ )
+
+ # Name
+ sub.add_argument(
+ '-n',
+ '--name',
+ help='Set the repository name to NAME'
+ )
+
+ # Suites
+ sub.add_argument(
+ '--add-suite',
+ metavar='SUITE[,SUITE]',
+ help=(
+ 'Add the specified suite(s) to the repository. Multiple '
+ 'repositories should be separated with commas. NOTE: Legacy deb '
+ 'repositories may only contain one suite.'
+ )
+ )
+ sub.add_argument(
+ '--remove-suite',
+ metavar='SUITE[,SUITE]',
+ help=(
+ 'Remove the specified suite(s) from the repository. Multiple '
+ 'repositories should be separated with commas. NOTE: The last '
+ 'suite in a repository cannot be removed.'
+ )
+ )
+
+ # Components
+ sub.add_argument(
+ '--add-component',
+ metavar='COMPONENT[,COMPONENT]',
+ help=(
+ 'Add the specified component(s) to the repository. Multiple '
+ 'repositories should be separated with commas.'
+ )
+ )
+ sub.add_argument(
+ '--remove-component',
+ metavar='COMPONENT[,COMPONENT]',
+ help=(
+ 'Remove the specified component(s) from the repository. Multiple '
+ 'repositories should be separated with commas. NOTE: The last '
+ 'component in a repository cannot be removed.'
+ )
+ )
+
+ # URIs
+ sub.add_argument(
+ '--add-uri',
+ metavar='URI[,URI]',
+ help=(
+ 'Add the specified URI(s) to the repository. Multiple '
+ 'repositories should be separated with commas. NOTE: Legacy deb '
+ 'repositories may only contain one uri.'
+ )
+ )
+ sub.add_argument(
+ '--remove-uri',
+ metavar='URI[,URI]',
+ help=(
+ 'Remove the specified URI(s) from the repository. Multiple '
+ 'repositories should be separated with commas. NOTE: The last '
+ 'uri in a repository cannot be removed.'
+ )
+ )
+
+ # Options
+ sub.add_argument(
+ '--add-option',
+ metavar='OPTION=VALUE[,OPTION=VALUE]',
+ help=SUPPRESS
+ )
+ sub.add_argument(
+ '--remove-option',
+ metavar='OPTION=VALUE[,OPTION=VALUE]',
+ help=SUPPRESS
+ )
+
+ def finalize_options(self, args):
+ super().finalize_options(args)
+ self.count = 0
+ self.repo = ' '.join(args.repository)
+ self.enable = args.enable
+ self.disable = args.disable
+ self.source_enable = args.source_enable
+ self.source_disable = args.source_disable
+
+ self.actions:dict = {}
+
+ self.actions['endisable'] = None
+ for i in ['enable', 'disable']:
+ if getattr(args, i):
+ self.actions['endisable'] = i
+
+ self.actions['source_endisable'] = None
+ for i in ['source_enable', 'source_disable']:
+ if getattr(args, i):
+ self.actions['source_endisable'] = i
+
+ for arg in [
+ 'default_mirror',
+ 'name',
+ 'add_uri',
+ 'add_suite',
+ 'add_component',
+ 'remove_uri',
+ 'remove_suite',
+ 'remove_component',
+ ]:
+ self.actions[arg] = getattr(args, arg)
+
+ system.load_all_sources()
+
+ def run(self):
+ """Run the command"""
+ self.log.info('Modifying repository %s', self.repo)
+
+ self.system_source = False
+ try:
+ self.source = util.sources[self.repo]
+ except KeyError:
+ self.log.error(
+ 'The source %s could not be found. Check the spelling',
+ self.repo
+ )
+ return False
+
+ if self.source.ident == 'system':
+ self.system_source = True
+
+ self.log.debug('Actions to take:\n%s', self.actions)
+ self.log.debug('Source before:\n%s', self.source)
+
+ rets = []
+ for action in self.actions:
+ ret = getattr(self, action)(self.actions[action])
+ rets.append(ret)
+
+ self.log.debug('Results: %s', rets)
+ self.log.debug('Source after: \n%s', self.source)
+
+ if True in rets:
+ self.source.file.save()
+ return True
+ else:
+ self.log.warning('No valid changes specified, no actions taken.')
+ return False
+
+ def default_mirror(self, value:str) -> bool:
+ """Checks if this is the system source, then set the default mirror"""
+ if not value:
+ return False
+
+ if self.system_source:
+ self.source['X-Repolib-Default-Mirror'] = value
+ return True
+ return False
+
+ def name(self, value:str) -> bool:
+ """Sets the source name"""
+ if not value:
+ return False
+
+ self.log.info('Setting name for %s to %s', self.repo, value)
+ self.source.name = value
+ return True
+
+ def endisable(self, value:str) -> bool:
+ """Enable or disable the source"""
+ if not value:
+ return False
+
+ self.log.info('%sing source %s', value[:-1], self.repo)
+ if value == 'disable':
+ self.source.enabled = False
+ return True
+
+ self.source.enabled = True
+ return True
+
+ def source_endisable(self, value:str) -> bool:
+ """Enable/disable source code for the repo"""
+ if not value:
+ return False
+
+ self.log.info('%sing source code for source %s', value[7:-1], self.repo)
+ if value == 'source_disable':
+ self.source.sourcecode_enabled = False
+ return True
+
+ self.source.sourcecode_enabled = True
+ return True
+
+ def add_uri(self, values:str) -> bool:
+ """Adds URIs to the source, if not already present."""
+ if not values:
+ return False
+
+ self.log.info('Adding URIs: %s', values)
+ uris = self.source.uris
+
+ for uri in values.split():
+ if not util.url_validator(uri):
+ raise RepolibCommandError(
+ f'Cannot add URI {uri} to {self.repo}. The URI is '
+ 'malformed'
+ )
+
+ if uri not in uris:
+ uris.append(uri)
+ self.log.debug('Added URI %s', uri)
+
+ else:
+ self.log.warning(
+ 'The URI %s was already present in %s',
+ uri,
+ self.repo
+ )
+
+ if uris != self.source.uris:
+ self.source.uris = uris
+ return True
+ return False
+
+ def remove_uri(self, values:str) -> bool:
+ """Remove URIs from the soruce, if they are added."""
+ if not values:
+ return False
+
+ self.log.info('Removing URIs %s from source %s', values, self.repo)
+ uris = self.source.uris
+ self.log.debug('Starting uris: %s', uris)
+
+ for uri in values.split():
+ try:
+ uris.remove(uri)
+ self.log.debug('Removed URI %s', uri)
+
+ except ValueError:
+ self.log.warning(
+ 'The URI %s was not present in %s',
+ uri,
+ self.repo
+ )
+
+ if len(uris) == 0:
+ self.log.error(
+ 'Cannot remove the last URI from %s. If you meant to delete the source, try REMOVE instead.',
+ self.repo
+ )
+ return False
+
+ if uris != self.source.uris:
+ self.source.uris = uris
+ return True
+
+ return False
+
+ def add_suite(self, values:str) -> bool:
+ """Adds a suite to the source"""
+ if not values:
+ return False
+
+ self.log.info('Adding suites: %s', values)
+ suites = self.source.suites
+
+ for suite in values.split():
+ if suite not in suites:
+ suites.append(suite)
+ self.log.debug('Added suite %s', suite)
+
+ else:
+ self.log.warning(
+ 'The suite %s was already present in %s',
+ suite,
+ self.repo
+ )
+
+ if suites != self.source.suites:
+ self.source.suites = suites
+ return True
+ return False
+
+ def remove_suite(self, values:str) -> bool:
+ """Remove a suite from the source"""
+ if not values:
+ return False
+
+ self.log.info('Removing suites %s from source %s', values, self.repo)
+ suites = self.source.suites
+ self.log.debug('Starting suites: %s', suites)
+
+ for suite in values.split():
+ try:
+ suites.remove(suite)
+ self.log.debug('Removed suite %s', suite)
+
+ except ValueError:
+ self.log.warning(
+ 'The suite %s was not present in %s',
+ suite,
+ self.repo
+ )
+
+ if len(suites) == 0:
+ self.log.error(
+ 'Cannot remove the last suite from %s. If you meant to delete the source, try REMOVE instead.',
+ self.repo
+ )
+ return False
+
+ if suites != self.source.suites:
+ self.source.suites = suites
+ return True
+
+ return False
+
+ def add_component(self, values:str) -> bool:
+ """Adds components to the source"""
+ if not values:
+ return False
+
+ self.log.info('Adding components: %s', values)
+ components = self.source.components
+
+ for component in values.split():
+ if component not in components:
+ components.append(component)
+ self.log.debug('Added component %s', component)
+
+ else:
+ self.log.warning(
+ 'The component %s was already present in %s',
+ component,
+ self.repo
+ )
+
+ if len(components) > 1:
+ if self.source.file.format == util.SourceFormat.LEGACY:
+ self.log.warning(
+ 'Adding multiple components to a legacy source is not '
+ 'supported. Consider converting the source to DEB822 format.'
+ )
+
+ if components != self.source.components:
+ self.source.components = components
+ return True
+ return False
+
+ def remove_component(self, values:str) -> bool:
+ """Removes components from the source"""
+ if not values:
+ return False
+
+ self.log.info('Removing components %s from source %s', values, self.repo)
+ components = self.source.components
+ self.log.debug('Starting components: %s', components)
+
+ for component in values.split():
+ try:
+ components.remove(component)
+ self.log.debug('Removed component %s', component)
+
+ except ValueError:
+ self.log.warning(
+ 'The component %s was not present in %s',
+ component,
+ self.repo
+ )
+
+ if len(components) == 0:
+ self.log.error(
+ 'Cannot remove the last component from %s. If you meant to delete the source, try REMOVE instead.',
+ self.repo
+ )
+ return False
+
+ if components != self.source.components:
+ self.source.components = components
+ return True
+
+ return False
+
+ def add_option(self, values) -> bool:
+ """TODO: Support options"""
+ raise NotImplementedError(
+ 'Options have not been implemented in this version of repolib yet. '
+ f'Please edit the file {self.source.file.path} manually.'
+ )
+
+
+ def remove_option(self, values) -> bool:
+ """TODO: Support options"""
+ raise NotImplementedError(
+ 'Options have not been implemented in this version of repolib yet. '
+ f'Please edit the file {self.source.file.path} manually.'
+ )
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/command/remove.py b/repolib/usr/lib/python3/dist-packages/repolib/command/remove.py
new file mode 100644
index 0000000..851be03
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/command/remove.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from .. import util, system
+from .command import Command
+
+class Remove(Command):
+ """Remove subcommand
+
+ Removes configured sources from the system
+
+ Options:
+ --assume-yes, -y
+ """
+
+ @classmethod
+ def init_options(cls, subparsers):
+ """Sets up the argument parser for this command
+
+ Returns: argparse.subparser
+ This command's subparser
+ """
+
+ sub = subparsers.add_parser(
+ 'remove',
+ help='Remove a configured repository'
+ )
+
+ sub.add_argument(
+ 'repository',
+ help='The identifier of the repository to remove. See LIST'
+ )
+ sub.add_argument(
+ '-y',
+ '--assume-yes',
+ action='store_true',
+ help='Remove without prompting for confirmation'
+ )
+
+ def finalize_options(self, args):
+ super().finalize_options(args)
+ system.load_all_sources()
+ self.source_name = args.repository
+ self.assume_yes = args.assume_yes
+ self.source = None
+
+ def run(self):
+ """Run the command"""
+
+ self.log.info('Looking up %s for removal', self.source_name)
+
+ if self.source_name == 'system':
+ self.log.error('You cannot remove the system sources')
+ return False
+
+ if self.source_name not in util.sources:
+ self.log.error(
+ 'Source %s was not found. Double-check the spelling',
+ self.source_name
+ )
+ source_list:list = []
+ for source in util.sources:
+ source_list.append(source)
+ suggested_source:str = self.source_name.replace(':', '-')
+ suggested_source = suggested_source.translate(util.CLEAN_CHARS)
+ if not suggested_source in source_list:
+ return False
+
+ response:str = input(f'Did you mean "{suggested_source}"? (Y/n) ')
+ if not response:
+ response = 'y'
+ if response not in util.true_values:
+ return False
+ self.source_name = suggested_source
+
+ self.source = util.sources[self.source_name]
+ self.key = self.source.key
+ self.file = self.source.file
+
+ print(f'This will remove the source {self.source_name}')
+ print(self.source.ui)
+ response:str = 'n'
+ if self.assume_yes:
+ response = 'y'
+ else:
+ response = input('Are you sure you want to do this? (y/N) ')
+
+ if response in util.true_values:
+ self.file.remove_source(self.source_name)
+ self.file.save()
+
+ system.load_all_sources()
+ for source in util.sources.values():
+ self.log.debug('Checking key for %s', source.ident)
+ try:
+ if source.key.path == self.key.path:
+ self.log.info('Source key in use with another source')
+ return True
+ except AttributeError:
+ pass
+
+ self.log.info('No other sources found using key, deleting key')
+ if self.key:
+ self.key.delete_key()
+ return True
+
+ else:
+ print('Canceled.')
+ return False
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/file.py b/repolib/usr/lib/python3/dist-packages/repolib/file.py
new file mode 100644
index 0000000..5541a56
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/file.py
@@ -0,0 +1,544 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+import logging
+
+from pathlib import Path
+
+import dbus
+
+from .source import Source, SourceError
+from . import util
+
+FILE_COMMENT = "## Added/managed by repolib ##"
+
+class SourceFileError(util.RepoError):
+ """ Exception from a source file."""
+
+ def __init__(self, *args, code:int = 1, **kwargs):
+ """Exception with a source file
+
+ Arguments:
+ :code int, optional, default=1: Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code: int = code
+
+class SourceFile:
+ """ A Source File on disk
+
+ Attributes:
+ path(Pathlib.Path): the path for this file on disk
+ name(str): The name for this source (filename less the extension)
+ format(SourceFormat): The format used by this source file
+ contents(list): A list containing all of this file's contents
+ """
+
+ def __init__(self, name:str='') -> None:
+ """Initialize a source file
+
+ Arguments:
+ name(str): The filename within the sources directory to load
+ """
+ self.log = logging.getLogger(__name__)
+ self.name:str = ''
+ self.path:Path = Path()
+ self.alt_path:Path = Path()
+ self.format:util.SourceFormat = util.SourceFormat.DEFAULT
+ self.contents:list = []
+ self.sources:list = []
+
+ self.contents.append(FILE_COMMENT)
+ self.contents.append('#')
+
+ if name:
+ self.name = name
+ self.reset_path()
+
+ if self.path.exists():
+ self.contents = []
+ self.load()
+
+ def __str__(self):
+ return self.output
+
+ def __repr__(self):
+ return f'SourceFile(name={self.name})'
+
+ def add_source(self, source:Source) -> None:
+ """Adds a source to the file
+
+ Arguments:
+ source(Source): The source to add
+ """
+ if source not in self.sources:
+ self.contents.append(source)
+ self.sources.append(source)
+ source.file = self
+
+ def remove_source(self, ident:str) -> None:
+ """Removes a source from the file
+
+ Arguments:
+ ident(str): The ident of the source to remove
+ """
+ source = self.get_source_by_ident(ident)
+ self.contents.remove(source)
+ self.sources.remove(source)
+ self.save()
+
+ ## Remove sources prefs files/pin-priority
+ prefs_path = source.prefs
+ try:
+ if prefs_path.exists() and prefs_path.name:
+ prefs_path.unlink()
+
+ except AttributeError:
+ # No prefs path
+ pass
+
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.delete_prefs_file(str(prefs_path))
+
+ def get_source_by_ident(self, ident: str) -> Source:
+ """Find a source within this file by its ident
+
+ Arguments:
+ ident(str): The ident to search for
+
+ Returns: Source
+ The located source
+ """
+ self.log.debug(f'Looking up ident {ident} in {self.name}')
+ for source in self.sources:
+ if source.ident == ident:
+ self.log.debug(f'{ident} found')
+ return source
+ raise SourceFileError(
+ f'The file {self.path} does not contain the source {ident}'
+ )
+
+ def reset_path(self) -> None:
+ """Attempt to detect the correct path for this File.
+
+ We default to DEB822 .sources format files, but if that file doesn't
+ exist, fallback to legacy .list format. If this also doesn't exist, we
+ swap back to DEB822 format, as this is likely a new file."""
+ self.log.debug('Resetting path')
+
+ default_path = util.SOURCES_DIR / f'{self.name}.sources'
+ legacy_path = util.SOURCES_DIR / f'{self.name}.list'
+
+ if default_path.exists():
+ self.path = default_path
+ self.alt_path = legacy_path
+ self.format = util.SourceFormat.DEFAULT
+
+ elif legacy_path.exists():
+ self.path = legacy_path
+ self.alt_path = default_path
+ self.format = util.SourceFormat.LEGACY
+
+ else:
+ self.path = default_path
+ self.alt_path = legacy_path
+
+ return
+
+ def find_unique_ident(self, source1:Source, source2:Source) -> bool:
+ """Takes two sources with identical idents, and finds a new, unique
+ idents for them.
+
+ The rules for this are mildly complicated, and vary depending on the
+ situation:
+
+ * (DEB822) If the sources are identical other than some portion of
+ data, then the two will be combined into a single source.
+ * (legacy) If the two sources are identical other than source type
+ (common with legacy-format PPAs with source code) then the second
+ source will be dropped until export.
+ * (legacy) If the sources differ by URIs, Components, or Suites, then
+ the differing data will be appended to the sources' idents.
+ * (Either) If no other rules can be determined, then the sources will
+ have a number appended to them
+
+ Arguments:
+ source1(Source): The original source with the ident
+ source2(Source): The new colliding source with the ident
+
+ Returns: bool
+ `True` if the two sources were successfully deduped, `False` if the
+ second source should be discarded.
+ """
+ ident_src1:str = source1.ident
+ ident_src2:str = source2.ident
+
+ self.log.debug(f'Idents {ident_src1} and {ident_src2} conflict')
+
+ if self.format == util.SourceFormat.DEFAULT:
+ util.combine_sources(source1, source2)
+ ident_src2 = ''
+
+ else:
+ excl_keys = [
+ 'X-Repolib-Name',
+ 'X-Repolib-ID',
+ 'X-Repolib-Comment',
+ 'Enabled',
+ 'Types'
+ ]
+ if len(source1.types) == 1 and len(source2.types) == 1:
+ if util.compare_sources(source1, source2, excl_keys):
+ util.combine_sources(source1, source2)
+ source1.types = [
+ util.SourceType.BINARY, util.SourceType.SOURCECODE
+ ]
+ source1.twin_source = True
+ source1.sourcecode_enabled = source2.enabled
+ ident_src2 = ''
+ diffs = util.find_differences_sources(source1, source2, excl_keys)
+ if diffs:
+ for key in diffs:
+ raw_diffs:tuple = diffs[key]
+ diff1_list = raw_diffs[0].strip().split()
+ diff2_list = raw_diffs[1].strip().split()
+ for i in diff1_list:
+ if i not in diff2_list:
+ ident_src1 += f'-{i}'
+ break
+ for i in diff2_list:
+ if i not in diff1_list:
+ ident_src2 += f'-{i}'
+ break
+ if ident_src1 != ident_src2:
+ break
+ if ident_src2 and ident_src1 != ident_src2:
+ source1.ident = ident_src1
+ source2.ident = ident_src2
+ return True
+
+ elif ident_src2 and ident_src1 == ident_src2:
+ for source in self.sources:
+ src_index = self.sources.index(source)
+ source.ident = f'{self.name}-{src_index}'
+ return True
+
+ elif not ident_src2:
+ return False
+
+ return True
+
+
+ def load(self) -> None:
+ """Loads the sources from the file on disk"""
+ self.log.debug(f'Loading source file {self.path}')
+ self.contents = []
+ self.sources = []
+
+ if not self.name:
+ raise SourceFileError('You must provide a filename to load.')
+
+ if not self.path.exists():
+ raise SourceFileError(f'The file {self.path} does not exist.')
+
+ with open(self.path, 'r') as source_file:
+ srcfile_data = source_file.readlines()
+
+ item:int = 0
+ raw822:list = []
+ parsing_deb822:bool = False
+ source_name:str = ''
+ commented:bool = False
+ idents:dict = {}
+
+ # Main file parsing loop
+ for line in srcfile_data:
+ comment_found:str = ''
+ name_line:bool = 'X-Repolib-Name' in line
+
+ if not parsing_deb822:
+ commented = line.startswith('#')
+
+ # Find commented out lines
+ if commented:
+ # Exclude disabled legacy deblines
+ valid_legacy = util.validate_debline(line.strip())
+ if not valid_legacy and not name_line:
+ # Found a standard comment
+ self.contents.append(line.strip())
+
+ elif valid_legacy:
+ if self.format != util.SourceFormat.LEGACY:
+ raise SourceFileError(
+ f'File {self.path.name} is an updated file, but '
+ 'contains legacy-format sources. This is not '
+ 'allowed. Please fix the file manually.'
+ )
+ new_source = Source()
+ new_source.load_from_data([line])
+ if source_name:
+ new_source.name = source_name
+ if not new_source.ident:
+ new_source.ident = self.name
+ to_add:bool = True
+ if new_source.ident in idents:
+ old_source = idents[new_source.ident]
+ idents.pop(old_source.ident)
+ to_add = self.find_unique_ident(old_source, new_source)
+ idents[old_source.ident] = old_source
+ idents[new_source.ident] = new_source
+ if to_add:
+ new_source.file = self
+ self.contents.append(new_source)
+ self.sources.append(new_source)
+
+ elif name_line:
+ source_name = ':'.join(line.split(':')[1:])
+ source_name = source_name.strip()
+
+ # Active legacy line
+ elif not commented:
+ if util.validate_debline(line.strip()):
+ if self.format != util.SourceFormat.LEGACY:
+ raise SourceFileError(
+ f'File {self.path.name} is an updated file, but '
+ 'contains legacy-format sources. This is not '
+ 'allowed. Please fix the file manually.'
+ )
+ new_source = Source()
+ new_source.load_from_data([line])
+ if source_name:
+ new_source.name = source_name
+ if not new_source.ident:
+ new_source.ident = self.name
+ to_add:bool = True
+ if new_source.ident in idents:
+ old_source = idents[new_source.ident]
+ idents.pop(old_source.ident)
+ to_add = self.find_unique_ident(old_source, new_source)
+ idents[old_source.ident] = old_source
+ idents[new_source.ident] = new_source
+ if to_add:
+ new_source.file = self
+ self.contents.append(new_source)
+ self.sources.append(new_source)
+
+ # Empty lines are treated as comments
+ if line.strip() == '':
+ self.contents.append('')
+
+ # Find 822 sources
+ # Valid sources can begin with any key:
+ for key in util.valid_keys:
+ if line.startswith(key):
+ if self.format == util.SourceFormat.LEGACY:
+ raise SourceFileError(
+ f'File {self.path.name} is a DEB822-format file, but '
+ 'contains legacy sources. This is not allowed. '
+ 'Please fix the file manually.'
+ )
+ parsing_deb822 = True
+ raw822.append(line.strip())
+
+ item += 1
+
+ elif parsing_deb822:
+ # Deb822 sources are terminated with an empty line
+ if line.strip() == '':
+ parsing_deb822 = False
+ new_source = Source()
+ new_source.load_from_data(raw822)
+ new_source.file = self
+ if source_name:
+ new_source.name = source_name
+ if not new_source.ident:
+ new_source.ident = self.name
+ if new_source.ident in idents:
+ old_source = idents[new_source.ident]
+ idents.pop(old_source.ident)
+ self.find_unique_ident(old_source, new_source)
+ idents[old_source.ident] = old_source
+ idents[new_source.ident] = new_source
+ new_source.file = self
+ self.contents.append(new_source)
+ self.sources.append(new_source)
+ raw822 = []
+ item += 1
+ self.contents.append('')
+ else:
+ raw822.append(line.strip())
+
+ if raw822:
+ parsing_deb822 = False
+ new_source = Source()
+ new_source.load_from_data(raw822)
+ new_source.file = self
+ if source_name:
+ new_source.name = source_name
+ if not new_source.ident:
+ new_source.ident = self.name
+ if new_source.ident in idents:
+ old_source = idents[new_source.ident]
+ idents.pop(old_source.ident)
+ self.find_unique_ident(old_source, new_source)
+ idents[old_source.ident] = old_source
+ idents[new_source.ident] = new_source
+ new_source.file = self
+ self.contents.append(new_source)
+ self.sources.append(new_source)
+ raw822 = []
+ item += 1
+ self.contents.append('')
+
+ for source in self.sources:
+ if not source.has_required_parts:
+ raise SourceFileError(
+ f'The file {self.path.name} is malformed and contains '
+ 'errors. Maybe it has some extra new-lines?'
+ )
+
+ self.log.debug('File %s loaded', self.path)
+
+ def save(self) -> None:
+ """Saves the source file to disk using the current format"""
+ self.log.debug(f'Saving source file to {self.path}')
+
+ for source in self.sources:
+ self.log.debug('New Source %s: \n%s', source.ident, source)
+
+ save_path = util.SOURCES_DIR / f'{self.name}.save'
+
+ for source in self.sources:
+ if source.key:
+ source.key.save_gpg()
+ source.tasks_save()
+
+ if not self.name or not self.format:
+ raise SourceFileError('There was not a complete filename to save')
+
+ if not util.SOURCES_DIR.exists():
+ try:
+ util.SOURCES_DIR.mkdir(parents=True)
+ except PermissionError:
+ self.log.error(
+ 'Source destination path does not exist and cannot be created '
+ 'Failures expected now.'
+ )
+
+ if len(self.sources) > 0:
+ self.log.debug('Saving, Main path %s; Alt path: %s', self.path, self.alt_path)
+ try:
+ with open(self.path, mode='w') as output_file:
+ output_file.write(self.output)
+ if self.alt_path.exists():
+ self.alt_path.rename(save_path)
+
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.output_file_to_disk(self.path.name, self.output)
+ self.log.debug('File %s saved', self.path)
+ else:
+ try:
+ self.path.unlink(missing_ok=True)
+ self.alt_path.unlink(missing_ok=True)
+ save_path.unlink(missing_ok=True)
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.delete_source_file(self.path.name)
+ self.log.debug('File %s removed', self.path)
+
+
+ ## Attribute properties
+ @property
+ def format(self) -> util.SourceFormat: # type: ignore (We don't use str.format)
+ """The format of the file on disk"""
+ return self._format
+
+ @format.setter
+ def format(self, format:util.SourceFormat) -> None: # type: ignore (We don't use str.format)
+ """The path needs to be updated when the format changes"""
+ alt_format:util.SourceFormat = util.SourceFormat.LEGACY
+ self._format = format
+ self.path = util.SOURCES_DIR / f'{self.name}.{self._format.value}'
+ for format_ in util.SourceFormat:
+ if format != format_:
+ alt_format = format_
+ self.alt_path = util.SOURCES_DIR / f'{self.name}.{alt_format.value}'
+
+ ## Output properties
+ @property
+ def legacy(self) -> str:
+ """Outputs the file in the output_legacy format"""
+ legacy_output:str = ''
+ for item in self.contents:
+ try:
+ legacy_output += item.legacy
+ except AttributeError:
+ legacy_output += item
+ legacy_output += '\n'
+ return legacy_output
+
+ @property
+ def deb822(self) -> str:
+ """Outputs the file in the output_822 format"""
+ deb822_output:str = ''
+ for item in self.contents:
+ try:
+ deb822_output += item.deb822
+ except AttributeError:
+ deb822_output += item
+ deb822_output += '\n'
+ return deb822_output
+
+
+ @property
+ def ui(self) -> str:
+ """Outputs the file in the output_ui format"""
+ ui_output:str = ''
+ for item in self.contents:
+ try:
+ ui_output += item.ui
+ except AttributeError:
+ pass # Skip file comments in UI mode
+ ui_output += '\n'
+ return ui_output
+
+
+ @property
+ def output(self) -> str:
+ """Outputs the file in the output format"""
+ default_output:str = ''
+ for item in self.contents:
+ try:
+ if self.format == util.SourceFormat.DEFAULT:
+ default_output += item.deb822
+ elif self.format == util.SourceFormat.LEGACY:
+ default_output += item.legacy
+ default_output += '\n'
+ except AttributeError:
+ default_output += item
+ default_output += '\n'
+ return default_output
+
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/key.py b/repolib/usr/lib/python3/dist-packages/repolib/key.py
new file mode 100644
index 0000000..766881b
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/key.py
@@ -0,0 +1,204 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+import shutil
+
+import dbus
+import gnupg
+from pathlib import Path
+from urllib import request
+
+from . import util
+
+SKS_KEYSERVER = 'https://keyserver.ubuntu.com/'
+SKS_KEYLOOKUP_PATH = 'pks/lookup?op=get&options=mr&exact=on&search=0x'
+
+class KeyFileError(util.RepoError):
+ """ Exceptions related to apt key files."""
+
+ def __init__(self, *args, code=1, **kwargs):
+ """Exceptions related to apt key files.
+
+ Arguments:
+ code (:obj:`int`, optional, default=1): Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code = code
+
+class SourceKey:
+ """A signing key for an apt source."""
+
+ def __init__(self, name:str = '') -> None:
+ self.log = logging.getLogger(__name__)
+ self.tmp_path = Path()
+ self.path = Path()
+ self.gpg = gnupg.GPG()
+ self.data = b''
+
+ if name:
+ self.reset_path(name=name)
+ self.setup_gpg()
+
+ def reset_path(self, name: str = '', path:str = '', suffix: str = 'archive-keyring') -> None:
+ """Set the path for this key
+
+ Arguments:
+ suffix(str): The suffix to append to the end of the name to get the
+ file name (default: 'archive-keyring')
+ name(str): The name of the source
+ path(str): The entire path to the key
+ """
+ self.log.info('Setting path')
+ if not name and not path:
+ raise KeyFileError('A name is required to set the path for this key')
+
+ if name:
+ file_name = f'{name}-{suffix}.gpg'
+ self.tmp_path = util.TEMP_DIR / file_name
+ self.path = util.KEYS_DIR / file_name
+ elif path:
+ self.path = Path(path)
+ self.tmp_path = util.TEMP_DIR / self.path.name
+
+ self.setup_gpg()
+
+ self.log.debug('Key Path: %s', self.path)
+ self.log.debug('Temp Path: %s', self.tmp_path)
+
+ def setup_gpg(self) -> None:
+ """Set up the GPG object for this key."""
+ self.log.info('Setting up GPG')
+ self.log.debug('Copying %s to %s', self.path, self.tmp_path)
+ try:
+ shutil.copy2(self.path, self.tmp_path)
+
+ except FileNotFoundError:
+ pass
+
+ self.gpg = gnupg.GPG(keyring=str(self.tmp_path))
+ self.log.debug('GPG Setup: %s', self.gpg.keyring)
+
+ def save_gpg(self) -> None:
+ """Saves the key to disk."""
+ self.log.info('Saving key file %s from %s', self.path, self.tmp_path)
+ self.log.debug('Key contents: %s', self.gpg.list_keys())
+ self.log.debug('Temp key exists? %s', self.tmp_path.exists())
+ if not util.KEYS_DIR.exists():
+ try:
+ util.KEYS_DIR.mkdir(parents=True)
+ except PermissionError:
+ self.log.error(
+ 'Key destination path does not exist and cannot be created '
+ 'Failures expected now.'
+ )
+ try:
+ shutil.copy(self.tmp_path, self.path)
+
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.install_signing_key(
+ str(self.tmp_path),
+ str(self.path)
+ )
+
+ def delete_key(self) -> None:
+ """Deletes the key file from disk."""
+ try:
+ self.tmp_path.unlink()
+ self.path.unlink()
+
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.delete_signing_key(str(self.path))
+
+ except FileNotFoundError:
+ pass
+
+ def load_key_data(self, **kwargs) -> None:
+ """Loads the key data from disk into the object for processing.
+
+ Each of the keyword options specifies one place to look to import key
+ data. Once one is successfully imported, the method returns, so passing
+ multiple won't import multiple keys.
+
+ Keyword Arguments:
+ raw(bytes): Raw data to import to the keyring
+ ascii(str): ASCII-armored key data to import directly
+ url(str): A URL to download key data from
+ fingerprint(str): A key fingerprint to download from `keyserver`
+ keyserver(str): A keyserver to download from.
+ keypath(str): The path on the keyserver from which to download.
+
+ NOTE: The keyserver and keypath args only affect the operation of the
+ `fingerprint` keyword.
+ """
+
+ if self.path.exists():
+ with open(self.path, mode='rb') as keyfile:
+ self.data = keyfile.read()
+ return
+
+ self.tmp_path.touch()
+
+ if 'raw' in kwargs:
+ self.data = kwargs['raw']
+ self.gpg.import_keys(self.data)
+ return
+
+ if 'ascii' in kwargs:
+ self.gpg.import_keys(kwargs['ascii'])
+ if self.tmp_path.exists():
+ with open(self.tmp_path, mode='rb') as keyfile:
+ self.data = keyfile.read()
+ return
+
+ if 'url' in kwargs:
+ req = request.Request(kwargs['url'])
+ with request.urlopen(req) as response:
+ self.data = response.read().decode('UTF-8')
+ self.gpg.import_keys(self.data)
+ return
+
+ if 'fingerprint' in kwargs:
+ if not 'keyserver' in kwargs:
+ kwargs['keyserver'] = SKS_KEYSERVER
+
+ if not 'keypath' in kwargs:
+ kwargs['keypath'] = SKS_KEYLOOKUP_PATH
+
+ key_url = kwargs['keyserver'] + kwargs['keypath'] + kwargs['fingerprint']
+ req = request.Request(key_url)
+ with request.urlopen(req) as response:
+ self.data = response.read().decode('UTF-8')
+ self.gpg.import_keys(self.data)
+ return
+
+ raise TypeError(
+ f'load_key_data() got an unexpected keyword argument "{kwargs.keys()}',
+ ' Expected keyword arguments are: [raw, ascii, url, fingerprint]'
+ )
+
+
+
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/parsedeb.py b/repolib/usr/lib/python3/dist-packages/repolib/parsedeb.py
new file mode 100644
index 0000000..1e0d992
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/parsedeb.py
@@ -0,0 +1,371 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+
+from . import util
+
+log = logging.getLogger(__name__)
+
+class DebParseError(util.RepoError):
+ """ Exceptions related to parsing deb lines."""
+
+ def __init__(self, *args, code=1, **kwargs):
+ """Exceptions related to parsing deb lines.
+
+ Arguments:
+ code (:obj:`int`, optional, default=1): Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code = code
+
+def debsplit(line:str) -> list:
+ """ Improved string.split() with support for things like [] options.
+
+ Adapted from python-apt
+
+ Arguments:
+ line(str): The line to split up.
+ """
+ line = line.strip()
+ line_list = line.split()
+ for i in line_list:
+ if util.url_validator(i):
+ line_list[line_list.index(i)] = decode_brackets(i)
+ line = ' '.join(line_list)
+ pieces:list = []
+ tmp:str = ""
+ # we are inside a [..] block
+ p_found = False
+ for char in line:
+ if char == '[':
+ p_found = True
+ tmp += char
+ elif char == ']':
+ p_found = False
+ tmp += char
+ elif char.isspace() and not p_found:
+ pieces.append(tmp)
+ tmp = ''
+ continue
+ else:
+ tmp += char
+ # append last piece
+ if len(tmp) > 0:
+ pieces.append(tmp)
+ return pieces
+
+def encode_brackets(word:str) -> str:
+ """ Encodes any [ and ] brackets into URL-safe form
+
+ Technically we should never be recieving these, and there are other things
+ which should technically be encoded as well. However, square brackets
+ actively break the URL parsing, and must be strictly avoided.
+
+ Arguments:
+ word (str): the string to encode brackets in.
+
+ Returns:
+ `str`: the encoded string.
+ """
+ word = word.replace('[', '%5B')
+ word = word.replace(']', '%5D')
+ return word
+
+def decode_brackets(word:str) -> str:
+ """ Un-encodes [ and ] from the input
+
+ Since our downstream libraries should also be encoding these correctly, it
+ is better to retain these as the user entered, as that ensures they can
+ recognize it properly.
+
+ Arguments:
+ word (str): The string to decode.
+
+ Returns:
+ `str`: the decoded string.
+ """
+ word = word.replace('%5B', '[')
+ word = word.replace('%5D', ']')
+ return word
+
+def parse_name_ident(tail:str) -> tuple:
+ """ Find a Repolib name within the given comment string.
+
+ The name should be headed with "X-Repolib-Name:" and is not space terminated.
+ The ident should be headed with "X-Repolib-ID:" and is space terminated.
+
+ Either field ends at the end of a line, or at a subsequent definition of a
+ different field, or at a subsequent ' #' substring. Additionally, the ident
+ field ends with a subsequent space.
+
+ Arguments:
+ tail (str): The comment to search within.
+
+ Returns: tuple(name, ident, comment):
+ name (str): The detected name, or None
+ ident (str): The detected ident, or None
+ comment (str): The string with the name and ident removed
+ """
+ tail = util.strip_hashes(tail)
+
+ # Used for sanity checking later
+ has_name = 'X-Repolib-Name' in tail
+ log.debug('Line name found: %s', has_name)
+ has_ident = 'X-Repolib-ID' in tail
+ log.debug('Line ident found: %s', has_ident)
+
+ parts: list = tail.split()
+ name_found = False
+ ident_found = False
+ name:str = ''
+ ident:str = ''
+ comment:str = ''
+ for item in parts:
+ log.debug("Checking line item: %s", item)
+ item_is_name = item.strip('#').strip().startswith('X-Repolib-Name')
+ item_is_ident = item.strip('#').strip().startswith('X-Repolib-ID')
+
+ if '#' in item and not item_is_name and not item_is_ident:
+ name_found = False
+ ident_found = False
+
+ elif item_is_name:
+ name_found = True
+ ident_found = False
+ continue
+
+ elif item_is_ident:
+ name_found = False
+ ident_found = True
+ continue
+
+ if name_found and not item_is_name:
+ name += f'{item} '
+ continue
+
+ elif ident_found and not item_is_ident:
+ ident += f'{item}'
+ ident_found = False
+ continue
+
+ elif not name_found and not ident_found:
+ c = item.strip('#')
+ comment += f'{c} '
+
+ name = name.strip()
+ ident = ident.strip()
+ comment = comment.strip()
+
+ if not name:
+ if ident:
+ name = ident
+
+ # Final sanity checking
+ if has_name and not name:
+ raise DebParseError(
+ f'Could not parse repository name from comment {comment}. Make sure '
+ 'you have a space between the colon and the Name'
+ )
+ if has_ident and not ident:
+ raise DebParseError(
+ f'Could not parse repository ident from comment {comment}. Make sure '
+ 'you have a space between the colon and the Ident'
+ )
+
+ return name, ident, comment
+
+
+class ParseDeb:
+ """ Parsing for source entries.
+
+ Contains parsing helpers for one-line format sources.
+ """
+
+ def __init__(self, debug:bool = False) -> None:
+ """
+ Arguments:
+ debug (bool): In debug mode, the structured data is always returned
+ at the end, instead of checking for sanity (default: `False`)
+ """
+ self.debug = debug
+ self.last_line: str = ''
+ self.last_line_valid: bool = False
+ self.curr_line: str = ''
+ self.curr_line_valid: bool = False
+
+ def parse_options(self, opt:str) -> dict:
+ """ Parses a string of options into a dictionary that repolib can use.
+
+ Arguments:
+ opt(str): The string with options returned from the line parser.
+
+ Returns:
+ `dict`: The dictionary of options with key:val pairs (may be {})
+ """
+ opt = opt.strip()
+ opt = opt[1:-1].strip() # Remove enclosing brackets
+ options = opt.split()
+
+ parsed_options:dict = {}
+
+ for opt in options:
+ pre_key, values = opt.split('=')
+ values = values.split(',')
+ value:str = ' '.join(values)
+ try:
+ key:str = util.options_inmap[pre_key]
+ except KeyError:
+ raise DebParseError(
+ f'Could not parse line {self.curr_line}: option {opt} is '
+ 'not a valid debian repository option or is unsupported.'
+ )
+ parsed_options[key] = value
+
+ return parsed_options
+
+
+ def parse_line(self, line:str) -> dict:
+ """ Parse a deb line into its individual parts.
+
+ Adapted from python-apt
+
+ Arguments:
+ line (str): The line input to parse
+
+ Returns:
+ (dict): a dict containing the requisite data.
+ """
+ self.last_line = self.curr_line
+ self.last_line_valid = self.curr_line_valid
+ self.curr_line = line.strip()
+ parts:list = []
+
+ line_is_comment = self.curr_line == '#'
+ line_is_empty = self.curr_line == ''
+ if line_is_comment or line_is_empty:
+ raise DebParseError(f'Current line "{self.curr_line}" is empty')
+
+ line_parsed: dict = {}
+ line_parsed['enabled'] = True
+ line_parsed['name'] = ''
+ line_parsed['ident'] = ''
+ line_parsed['comments'] = []
+ line_parsed['repo_type'] = ''
+ line_parsed['uri'] = ''
+ line_parsed['suite'] = ''
+ line_parsed['components'] = []
+ line_parsed['options'] = {}
+
+ if line.startswith('#'):
+ line_parsed['enabled'] = False
+ line = util.strip_hashes(line)
+ parts = line.split()
+ if not parts[0] in ('deb', 'deb-src'):
+ raise DebParseError(f'Current line "{self.curr_line}" is invalid')
+
+ comments_index = line.find('#')
+ if comments_index > 0:
+ raw_comments:str = line[comments_index + 1:].strip()
+ (
+ line_parsed['name'],
+ line_parsed['ident'],
+ comments
+ ) = parse_name_ident(raw_comments)
+ line_parsed['comments'].append(comments)
+ line = line[:comments_index]
+
+ parts = debsplit(line)
+ if len(parts) < 3: # We need at least a type, a URL, and a component
+ raise DebParseError(
+ f'The line "{self.curr_line}" does not have enough pieces to be'
+ 'valid'
+ )
+ # Determine the type of the repo
+ repo_type:str = parts.pop(0)
+ if repo_type in ['deb', 'deb-src']:
+ line_parsed['repo_type'] = util.SourceType(repo_type)
+ else:
+ raise DebParseError(f'The line "{self.curr_line}" is of invalid type.')
+
+ # Determine the properties of our repo line
+ uri_index:int = 0
+ is_cdrom: bool = False
+ ## The URI index is the vital piece of information we need to parse the
+ ## deb line, as it's position determines what other components are
+ ## present and where they are. This determines the location of the URI
+ ## regardless of where it's at.
+ for part in parts:
+ if part.startswith('['):
+ if 'cdrom' in part:
+ is_cdrom = True
+ uri_index = parts.index(part)
+ else:
+ uri_index = 1
+
+ if is_cdrom:
+ # This could maybe change if the parser now differentiates between
+ # CDROM URIs and option lists
+ raise DebParseError('Repolib cannot currently accept CDROM Sources')
+
+ if uri_index != 0:
+ line_parsed['options'] = self.parse_options(parts.pop(0))
+
+ if len(line_parsed) < 2: # Should have at minimum a URI and a suite/path
+ raise DebParseError(
+ f'The line "{self.curr_line}" does not have enough pieces to be'
+ 'valid'
+ )
+
+ line_uri = parts.pop(0)
+ if util.url_validator(line_uri):
+ line_parsed['uri'] = line_uri
+
+ else:
+ raise DebParseError(
+ f'The line "{self.curr_line}" has invalid URI: {line_uri}'
+ )
+
+ line_parsed['suite'] = parts.pop(0)
+
+ line_components:list = []
+ for comp in parts:
+ line_parsed['components'].append(comp)
+
+
+ has_type = line_parsed['repo_type']
+ has_uri = line_parsed['uri']
+ has_suite = line_parsed['suite']
+
+ if has_type and has_uri and has_suite:
+ # if we have these three minimum components, we can proceed and the
+ # line is valid. Otherwise, error out.
+ return line_parsed.copy()
+
+ if self.debug:
+ return line_parsed.copy()
+
+ raise DebParseError(
+ f'The line {self.curr_line} could not be parsed due to an '
+ 'unknown error (Probably missing the repo type, URI, or a '
+ 'suite/path).'
+ )
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/__init__.py b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/__init__.py
new file mode 100644
index 0000000..43c1503
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/__init__.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+from .popdev import PopdevSource
+from .ppa import PPASource
+from ..source import Source
+
+shortcut_prefixes = {
+ 'deb': Source,
+ 'deb-src': Source,
+ ppa.prefix: ppa.PPASource,
+ popdev.prefix: popdev.PopdevSource
+}
\ No newline at end of file
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/popdev.py b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/popdev.py
new file mode 100644
index 0000000..f4efc6e
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/popdev.py
@@ -0,0 +1,171 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+from pathlib import Path
+
+import dbus
+
+from repolib.key import SourceKey
+
+from ..source import Source, SourceError
+from ..file import SourceFile
+from .. import util
+
+BASE_FORMAT = util.SourceFormat.DEFAULT
+BASE_URL = 'http://apt.pop-os.org/staging'
+BASE_COMPS = 'main'
+BASE_KEYURL = 'https://raw.githubusercontent.com/pop-os/pop/master/scripts/.iso.asc'
+
+DEFAULT_FORMAT = util.SourceFormat.DEFAULT
+
+prefix = 'popdev'
+delineator = ':'
+
+class PopdevSource(Source):
+ """ PopDev Source shortcut
+
+ These are given in the format popdev:branchname.
+
+ Arguments:
+ shortcut (str): The ppa: shortcut to process
+ """
+ prefs_dir = Path('/etc/apt/preferences.d')
+ default_format = BASE_FORMAT
+
+ @staticmethod
+ def validator(shortcut:str) -> bool:
+ """Determine whether a PPA shortcut is valid.
+
+ Arguments:
+ shortcut(str): The shortcut to validate
+
+ Returns: bool
+ `True` if the PPA is valid, otherwise False
+ """
+ if '/' in shortcut:
+ return False
+
+ shortcut_split = shortcut.split(':')
+ try:
+ if not shortcut_split[1]:
+ return False
+ except IndexError:
+ return False
+
+ if shortcut.startswith(f'{prefix}:'):
+ shortlist = shortcut.split(':')
+ if len(shortlist) > 0:
+ return True
+
+ return False
+
+ def __init__(self, *args, line='', fetch_data=True, **kwargs):
+ if line:
+ if not line.startswith('ppa:'):
+ raise SourceError(f'The PPA shortcut {line} is malformed')
+ super().__init__(args, kwargs)
+ self.log = logging.getLogger(__name__)
+ self.line = line
+ self.twin_source:bool = True
+ self.prefs_path = None
+ self.branch_name:str = ''
+ self.branch_url:str = ''
+ if line:
+ self.load_from_shortcut(line)
+
+ def tasks_save(self, *args, **kwargs) -> None:
+ super().tasks_save(*args, **kwargs)
+ self.log.info('Saving prefs file for %s', self.ident)
+ prefs_contents = 'Package: *\n'
+ prefs_contents += f'Pin: release o=pop-os-staging-{self.branch_url}\n'
+ prefs_contents += 'Pin-Priority: 1002\n'
+
+ self.log.debug('%s prefs for pin priority:\n%s', self.ident, prefs_contents)
+
+ try:
+ with open(self.prefs, mode='w') as prefs_file:
+ prefs_file.write(prefs_contents)
+ except PermissionError:
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.output_prefs_to_disk(str(self.prefs), prefs_contents)
+
+ self.log.debug('Pin priority saved for %s', self.ident)
+
+
+ def get_description(self) -> str:
+ return f'Pop Development Staging branch'
+
+
+ def load_from_data(self, data: list) -> None:
+ self.log.debug('Loading line %s', data[0])
+ self.load_from_shortcut(shortcut=data[0])
+
+ def load_from_shortcut(self, shortcut:str='', meta:bool=True, get_key:bool=True) -> None:
+ """Translates the shortcut line into a full repo.
+
+ Arguments:
+ shortcut(str): The shortcut to load, if one hasn't been loaded yet.
+ """
+ self.reset_values()
+ if shortcut:
+ self.line = shortcut
+
+ if not self.line:
+ raise SourceError('No PPA shortcut provided')
+
+ if not self.validator(self.line):
+ raise SourceError(f'The line {self.line} is malformed')
+
+ self.log.debug('Loading shortcut %s', self.line)
+
+ self.info_parts = shortcut.split(delineator)
+ self.branch_url = ':'.join(self.info_parts[1:])
+ self.branch_name = util.scrub_filename(name=self.branch_url)
+ self.log.debug('Popdev branch name: %s', self.branch_name)
+
+ self.ident = f'{prefix}-{self.branch_name}'
+ if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
+ new_file = SourceFile(name=self.ident)
+ new_file.format = BASE_FORMAT
+ self.file = new_file
+ util.files[str(self.file.path)] = self.file
+ else:
+ self.file = util.files[str(self.file.path)]
+
+ self.file.add_source(self)
+
+ self.name = f'Pop Development Branch {self.branch_name}'
+ self.uris = [f'{BASE_URL}/{self.branch_url}']
+ self.suites = [util.DISTRO_CODENAME]
+ self.components = [BASE_COMPS]
+
+ key = SourceKey(name='popdev')
+ key.load_key_data(url=BASE_KEYURL)
+ self.key = key
+ self.signed_by = str(self.key.path)
+
+ self.prefs_path = self.prefs_dir / f'pop-os-staging-{self.branch_name}'
+ self.prefs = self.prefs_path
+
+ self.enabled = True
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/ppa.py b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/ppa.py
new file mode 100644
index 0000000..624ac4e
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/shortcuts/ppa.py
@@ -0,0 +1,274 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+
+from repolib.key import SourceKey
+
+from ..source import Source, SourceError
+from ..file import SourceFile
+from .. import util
+
+try:
+ from launchpadlib.launchpad import Launchpad
+ from lazr.restfulclient.errors import BadRequest, NotFound, Unauthorized
+except ImportError:
+ raise SourceError(
+ 'Missing optional dependency "launchpadlib". Try `sudo apt install '
+ 'python3-launchpadlib` to install it.'
+ )
+
+BASE_FORMAT = util.SourceFormat.LEGACY
+BASE_URL = 'http://ppa.launchpad.net'
+BASE_DIST = 'ubuntu'
+BASE_COMPS = 'main'
+
+DEFAULT_FORMAT = util.SourceFormat.LEGACY
+
+prefix = 'ppa'
+delineator = ':'
+
+class PPASource(Source):
+ """ PPA Source shortcut
+
+ These are given in the format ppa:owner/name. Much of this code is adapted
+ from SoftwareProperties.
+
+ Arguments:
+ shortcut (str): The ppa: shortcut to process
+ fetch_data (bool): Whether to try and fetch metadata from Launchpad.
+ """
+
+ default_format = BASE_FORMAT
+
+ @staticmethod
+ def validator(shortcut:str) -> bool:
+ """Determine whether a PPA shortcut is valid.
+
+ Arguments:
+ shortcut(str): The shortcut to validate
+
+ Returns: bool
+ `True` if the PPA is valid, otherwise False
+ """
+
+ if shortcut.startswith(f'{prefix}:'):
+ shortlist = shortcut.split('/')
+ if len(shortlist) > 1:
+ return True
+ return False
+
+ def __init__(self, *args, line='', fetch_data=True, **kwargs):
+ if line:
+ if not line.startswith('ppa:'):
+ raise SourceError(f'The PPA shortcut {line} is malformed')
+ super().__init__(args, kwargs)
+ self.log = logging.getLogger(__name__)
+ self.line = line
+ self.ppa = None
+ self.twin_source = True
+ self._displayname = ''
+ self._description = ''
+ if line:
+ self.load_from_shortcut(self.line)
+
+ def get_description(self) -> str:
+ output:str = ''
+ output += self.displayname
+ output += '\n\n'
+ output += self.description
+ return output
+
+ def load_from_data(self, data: list) -> None:
+ self.load_from_shortcut(shortcut=data[0])
+
+ def load_from_shortcut(self, shortcut:str='', meta:bool=True, key:bool=True) -> None:
+ """Translates the shortcut line into a full repo.
+
+ Arguments:
+ shortcut(str): The shortcut to load, if one hasn't been loaded yet.
+ meta(bool): Whether to fetch repo metadata from Launchpad
+ key(bool): Whether to fetch and install a signing key
+ """
+ self.reset_values()
+ if shortcut:
+ self.line = shortcut
+
+ if not self.line:
+ raise SourceError('No PPA shortcut provided')
+
+ if not self.validator(self.line):
+ raise SourceError(f'The line {self.line} is malformed')
+
+ line = self.line.replace(prefix + delineator, '')
+ self.info_parts = line.split('/')
+ ppa_owner = self.info_parts[0]
+ ppa_name = self.info_parts[1]
+
+ self.ident = f'{prefix}-{ppa_owner}-{ppa_name}'
+ if f'{self.ident}.{BASE_FORMAT.value}' not in util.files:
+ new_file = SourceFile(name=self.ident)
+ new_file.format = BASE_FORMAT
+ self.file = new_file
+ util.files[str(self.file.path)] = self.file
+ else:
+ self.file = util.files[str(self.file.path)]
+
+ self.file.add_source(self)
+
+ self.name = self.ident
+ self.uris = [f'{BASE_URL}/{ppa_owner}/{ppa_name}/{BASE_DIST}']
+ self.suites = [util.DISTRO_CODENAME]
+ self.components = [BASE_COMPS]
+
+ if meta or key:
+ self.ppa = get_info_from_lp(ppa_owner, ppa_name)
+ self.displayname = self.ppa.displayname
+ self.description = self.ppa.description
+
+ if self.ppa and meta:
+ self.name = self.ppa.displayname
+
+ if self.ppa and key:
+ repo_key = SourceKey(name=self.ident)
+ if str(repo_key.path) not in util.keys:
+ repo_key.load_key_data(fingerprint=self.ppa.fingerprint)
+ util.keys[str(repo_key.path)] = repo_key
+ self.key:SourceKey = repo_key
+ else:
+ self.key = util.keys[repo_key.path]
+ self.signed_by = str(self.key.path)
+
+ self.enabled = True
+
+ @property
+ def displayname(self) -> str:
+ """The name of the PPA provided by launchpad"""
+ if self._displayname:
+ return self._displayname
+ if self.ppa:
+ self._displayname = self.ppa.displayname
+ return self._displayname
+
+ @displayname.setter
+ def displayname(self, displayname) -> None:
+ """Cache this for use without hitting LP"""
+ self._displayname = displayname
+
+ @property
+ def description(self) -> str:
+ """The description of the PPA provided by Launchpad"""
+ if self._description:
+ return self._description
+ if self.ppa:
+ self._description = self.ppa.description
+ return self._description
+
+ @description.setter
+ def description(self, desc) -> None:
+ """Cache this for use without hitting LP"""
+ self._description = desc
+
+class PPA:
+ """ An object to fetch data from PPAs.
+
+ Portions of this class were adapted from Software Properties
+ """
+
+ def __init__(self, teamname, ppaname):
+ self.teamname = teamname
+ self.ppaname = ppaname
+ self._lap = None
+ self._lpteam = None
+ self._lpppa = None
+ self._signing_key_data = None
+ self._fingerprint = None
+
+ @property
+ def lap(self):
+ """ The Launchpad Object."""
+ if not self._lap:
+ self._lap = Launchpad.login_anonymously(
+ f'{self.__module__}.{self.__class__.__name__}',
+ service_root='production',
+ version='devel'
+ )
+ return self._lap
+
+ @property
+ def lpteam(self):
+ """ The Launchpad object for the PPA's owner."""
+ if not self._lpteam:
+ try:
+ self._lpteam = self.lap.people(self.teamname) # type: ignore (This won't actually be unbound because of the property)
+ except NotFound as err:
+ msg = f'User/Team "{self.teamname}" not found'
+ raise SourceError(msg) from err
+ except Unauthorized as err:
+ msg = f'Invalid user/team name "{self.teamname}"'
+ raise SourceError(msg) from err
+ return self._lpteam
+
+ @property
+ def lpppa(self):
+ """ The Launchpad object for the PPA."""
+ if not self._lpppa:
+ try:
+ self._lpppa = self.lpteam.getPPAByName(name=self.ppaname)
+ except NotFound as err:
+ msg = f'PPA "{self.teamname}/{self.ppaname}"" not found'
+ raise SourceError(msg) from err
+ except BadRequest as err:
+ msg = f'Invalid PPA name "{self.ppaname}"'
+ raise SourceError(msg) from err
+ return self._lpppa
+
+ @property
+ def description(self) -> str:
+ """str: The description of the PPA."""
+ return self.lpppa.description or ''
+
+ @property
+ def displayname(self) -> str:
+ """ str: the fancy name of the PPA."""
+ return self.lpppa.displayname or ''
+
+ @property
+ def fingerprint(self):
+ """ str: the fingerprint of the signing key."""
+ if not self._fingerprint:
+ self._fingerprint = self.lpppa.signing_key_fingerprint
+ return self._fingerprint
+
+
+def get_info_from_lp(owner_name, ppa):
+ """ Attempt to get information on a PPA from launchpad over the internet.
+
+ Arguments:
+ owner_name (str): The Launchpad user owning the PPA.
+ ppa (str): The name of the PPA
+
+ Returns:
+ json: The PPA information as a JSON object.
+ """
+ ppa = PPA(owner_name, ppa)
+ return ppa
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/source.py b/repolib/usr/lib/python3/dist-packages/repolib/source.py
new file mode 100644
index 0000000..ba89573
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/source.py
@@ -0,0 +1,945 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+from pathlib import Path
+
+from debian import deb822
+
+from .parsedeb import ParseDeb
+from .key import SourceKey
+from . import util
+
+DEFAULT_FORMAT = util.SourceFormat.LEGACY
+
+class SourceError(util.RepoError):
+ """ Exception from a source object."""
+
+ def __init__(self, *args, code=1, **kwargs):
+ """Exception with a source object
+
+ Arguments:
+ code (:obj:`int`, optional, default=1): Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code = code
+
+class Source(deb822.Deb822):
+ """A DEB822 object representing a single software source.
+
+ Attributes:
+ ident(str): The unique id for this source
+ name(str): The user-readable name for this source
+ enabled(bool): Whether or not the source is enabled
+ types([SourceType]): A list of repository types for this source
+ uris([str]): A list of possible URIs for this source
+ suites([str]): A list of enabled suites for this source
+ components([str]): A list of enabled components for this source
+ comments(str): Comments for this source
+ signed_by(Path): The path to this source's key file
+ file(SourceFile): The file this source belongs to
+ key(SourceKey): The key which signs this source
+ """
+
+ default_format = DEFAULT_FORMAT
+
+ @staticmethod
+ def validator(shortcut:str) -> bool:
+ """Determine whether a deb line is valid.
+
+ Arguments:
+ shortcut(str): The shortcut to validate
+
+ Returns: bool
+ `True` if the PPA is valid, otherwise False
+ """
+ shortcut_list:list = shortcut.split()
+
+ if not shortcut.startswith('deb'):
+ return False
+
+ if not len(shortcut_list) > 3:
+ return False
+
+ if not util.validate_debline:
+ return False
+
+ if len(shortcut_list) == 3 and '/' not in shortcut_list[-1]:
+ return False
+
+ return True
+
+ def __init__(self, *args, file=None, **kwargs) -> None:
+ """Initialize this source object"""
+ self.log = logging.getLogger(__name__)
+ super().__init__(*args, **kwargs)
+ self.reset_values()
+ self.file = file
+ self.twin_source = False
+ self.twin_enabled = False
+
+ def __repr__(self):
+ """type: () -> str"""
+ # Append comments to the item
+ # if self.options:
+
+ if self.comments:
+ self['Comments'] = '# '
+ self['Comments'] += ' # '.join(self.comments)
+
+ rep:str = '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
+
+ rep:str = '{'
+ for key in self:
+ rep += f"{util.PRETTY_PRINT}'{key}': '{self[key]}', "
+
+ rep = rep[:-2]
+ rep += f"{util.PRETTY_PRINT.replace(' ', '')}"
+ rep += '}'
+
+ if self.comments:
+ self.pop('Comments')
+
+ return rep
+
+ def __bool__(self) -> bool:
+ has_uri:bool = len(self.uris) > 0
+ has_suite:bool = len(self.suites) > 0
+ has_component:bool = len(self.components) > 0
+
+ if has_uri and has_suite and has_component:
+ return True
+ return False
+
+
+ def get_description(self) -> str:
+ """Get a UI-compatible description for a source.
+
+ Returns: (str)
+ The formatted description.
+ """
+ return self.name
+
+ def get_key_info(self, halt_errors: bool = False) -> dict:
+ """ Get a dictionary containing information for the signing key for
+ this source.
+
+ Arguments:
+ halt_errors (bool): if there are errors or other unexpected data,
+ raise a SourceError exception. Otherwise, simply log a warning.
+
+ Returns: dict
+ The dictionary from gnupg with key info.
+ """
+ if self.key:
+ keys:list = self.key.gpg.list_keys()
+ if len(keys) > 1:
+ error_msg = (
+ f'The keyring for {self.ident} contains {len(keys)} keys'
+ '. Check the keyring object for details about the keys.'
+ )
+ if halt_errors:
+ raise SourceError(error_msg)
+ self.log.warning(error_msg)
+ return keys[0]
+
+ return {}
+
+ def reset_values(self) -> None:
+ """Reset the default values for all attributes"""
+ self.log.info('Resetting source info')
+ self.ident = ''
+ self.name = ''
+ self.enabled = True
+ self.types = [util.SourceType.BINARY]
+ self.uris = []
+ self.suites = []
+ self.components = []
+ self.comments = []
+ self.signed_by = None
+ self.architectures = ''
+ self.languages = ''
+ self.targets = ''
+ self.pdiffs = ''
+ self.by_hash = ''
+ self.allow_insecure = ''
+ self.allow_weak = ''
+ self.allow_downgrade_to_insecure = ''
+ self.trusted = ''
+ self.signed_by = ''
+ self.check_valid_until = ''
+ self.valid_until_min = ''
+ self.valid_until_max = ''
+ self.prefs = ''
+ self._update_legacy_options()
+ self.file = None
+ self.key = None
+
+ def load_from_data(self, data:list) -> None:
+ """Loads source information from the provided data
+
+ Should correctly load either a lecagy Deb line (optionally with
+ preceeding comment) or a DEB822 source.
+
+ Arguments:
+ data(list): the data to load into the source.
+ """
+ self.log.info('Loading source from data')
+ self.reset_values()
+
+ if util.validate_debline(data[0]): # Legacy Source
+ if len(data) > 1:
+ raise SourceError(
+ f'The source is a legacy source but contains {len(data)} entries. '
+ 'It may only contain one entry.'
+ )
+ deb_parser = ParseDeb()
+ parsed_debline = deb_parser.parse_line(data[0])
+ self.ident = parsed_debline['ident']
+ self.name = parsed_debline['name']
+ self.enabled = parsed_debline['enabled']
+ self.types = [parsed_debline['repo_type']]
+ self.uris = [parsed_debline['uri']]
+ self.suites = [parsed_debline['suite']]
+ self.components = parsed_debline['components']
+ for key in parsed_debline['options']:
+ self[key] = parsed_debline['options'][key]
+ self._update_legacy_options()
+ for comment in parsed_debline['comments']:
+ self.comments.append(comment)
+ if self.comments == ['']:
+ self.comments = []
+
+ if not self.name:
+ self.name = self.generate_default_name()
+
+ if self.signed_by:
+ self.load_key()
+ return
+
+ # DEB822 Source
+ super().__init__(sequence=data)
+ if self.signed_by:
+ self.load_key()
+ return
+
+ @property
+ def sourcecode_enabled(self) -> bool:
+ """`True` if this source also provides source code, otherwise `False`"""
+ if util.SourceType.SOURCECODE in self.types:
+ return True
+ return False
+
+ @sourcecode_enabled.setter
+ def sourcecode_enabled(self, enabled) -> None:
+ """Accept a variety of input values"""
+ types = [util.SourceType.BINARY]
+ if enabled in util.true_values:
+ types.append(util.SourceType.SOURCECODE)
+ self.types = types
+
+
+ def generate_default_ident(self, prefix='') -> str:
+ """Generates a suitable ID for the source
+
+ Returns: str
+ A sane default-id
+ """
+ ident:str = ''
+ if len(self.uris) > 0:
+ uri:str = self.uris[0].replace('/', ' ')
+ uri_list:list = uri.split()
+ uri_str:str = '-'.join(uri_list[1:])
+ branch_name:str = util.scrub_filename(uri_str)
+ ident = f'{prefix}{branch_name}'
+ ident += f'-{self.types[0].ident()}'
+ try:
+ if not self['X-Repolib-ID']:
+ self['X-Repolib-ID'] = ident
+ except KeyError:
+ self['X-Repolib-ID'] = ident
+ return ident
+
+ def generate_default_name(self) -> str:
+ """Generate a default name based on the ident
+
+ Returns: str
+ A name based on the ident
+ """
+ name:str = self.ident
+ try:
+ name = self['X-Repolib-Name']
+ except KeyError:
+ self['X-Repolib-Name'] = self.ident
+ return self['X-Repolib-Name']
+
+ if not name:
+ self['X-Repolib-Name'] = self.ident
+
+ return self['X-Repolib-Name']
+
+ def load_key(self, ignore_errors:bool = True) -> None:
+ """Finds and loads the signing key from the system
+
+ Arguments:
+ ignore_errors(bool): If `False`, throw a SourceError exception if
+ the key can't be found/doesn't exist (Default: `True`)
+ """
+ self.log.info('Finding key for source %s', self.ident)
+ if not self.signed_by:
+ if ignore_errors:
+ self.log.warning('No key configured for source %s', self.ident)
+ else:
+ raise SourceError('No key configured for source {self.ident}')
+
+ if self.signed_by not in util.keys:
+ new_key = SourceKey()
+ new_key.reset_path(path=self.signed_by)
+ self.key = new_key
+ util.keys[str(new_key.path)] = new_key
+ else:
+ self.key = util.keys[self.signed_by]
+
+ def save(self) -> None:
+ """Proxy method to save the source"""
+ if not self.file == None:
+ self.file.save()
+
+ def output_legacy(self) -> str:
+ """Outputs a legacy representation of this source
+
+ Note: this is expected to fail if there is more than one type, URI, or
+ Suite; the one-line format does not support having multiple of these
+ properties.
+
+ Returns: str
+ The source output formatted as Legacy
+ """
+ return self.legacy
+
+ def output_822(self) -> str:
+ """Outputs a DEB822 representation of this source
+
+ Returns: str
+ The source output formatted as Deb822
+ """
+ return self.deb822
+
+ def output_ui(self) -> str:
+ """Outputs a string representation of this source for use in UIs
+
+ Returns: str
+ The source output string
+ """
+ return self.ui
+
+ def prop_append(self, prop:list, item:str) -> None:
+ """Appends an item to a list property of this source.
+ NOTE: List properties are `types`, `uris`, `suites`, and `components`.
+
+ Arguments:
+ prop(list): The property on which to append the item.
+ item(str): The item to append to the propery
+ """
+ _list = prop
+ _list.append(item)
+ prop = _list
+
+ def tasks_save(self, *args, **kwargs) -> None:
+ """Extra tasks to perform when saving a source"""
+ return
+
+ ## Properties are stored/retrieved from the underlying Deb822 dict
+ @property
+ def has_required_parts(self) -> bool:
+ """(RO) True if all required attributes are set, otherwise false."""
+ required_parts = ['uris', 'suites', 'ident']
+
+ for attr in required_parts:
+ if len(getattr(self, attr)) < 1:
+ return False
+
+ return True
+
+
+ @property
+ def ident(self) -> str:
+ """The ident for this source within the file"""
+ try:
+ return self['X-Repolib-ID']
+ except KeyError:
+ return ''
+
+
+ @ident.setter
+ def ident(self, ident: str) -> None:
+ ident = util.scrub_filename(ident)
+ self['X-Repolib-ID'] = ident
+
+
+ @property
+ def name(self) -> str:
+ """The human-friendly name for this source"""
+ try:
+ _name = self['X-Repolib-Name']
+ except KeyError:
+ _name = ''
+
+ if not _name:
+ self.generate_default_name()
+ return self['X-Repolib-Name']
+
+ @name.setter
+ def name(self, name: str) -> None:
+ self['X-Repolib-Name'] = name
+
+
+ @property
+ def enabled(self) -> util.AptSourceEnabled:
+ """Whether or not the source is enabled/active"""
+ try:
+ enabled = self['Enabled'] in util.true_values
+ except KeyError:
+ return util.AptSourceEnabled.FALSE
+
+ if enabled and self.has_required_parts:
+ return util.AptSourceEnabled.TRUE
+ return util.AptSourceEnabled.FALSE
+
+ @enabled.setter
+ def enabled(self, enabled) -> None:
+ """For convenience, accept a wide varietry of input value types"""
+ self['Enabled'] = 'no'
+ if enabled in util.true_values:
+ self['Enabled'] = 'yes'
+
+
+ @property
+ def types(self) -> list:
+ """The list of source types for this source"""
+ _types:list = []
+ try:
+ for sourcetype in self['types'].split():
+ _types.append(util.SourceType(sourcetype))
+ except KeyError:
+ pass
+ return _types
+
+ @types.setter
+ def types(self, types: list) -> None:
+ """Turn this list into a string of values for storage"""
+ self['Types'] = ''
+ _types:list = []
+ for sourcetype in types:
+ if sourcetype not in _types:
+ _types.append(sourcetype)
+ for sourcetype in _types:
+ self['Types'] += f'{sourcetype.value} '
+ self['Types'] = self['Types'].strip()
+
+
+ @property
+ def uris(self) -> list:
+ """The list of URIs for this source"""
+ try:
+ return self['URIs'].split()
+ except KeyError:
+ return []
+
+ @uris.setter
+ def uris(self, uris: list) -> None:
+ self['URIs'] = ' '.join(uris).strip()
+
+
+ @property
+ def suites(self) -> list:
+ """The list of URIs for this source"""
+ try:
+ return self['Suites'].split()
+ except KeyError:
+ return []
+
+ @suites.setter
+ def suites(self, suites: list) -> None:
+ self['Suites'] = ' '.join(suites).strip()
+
+
+ @property
+ def components(self) -> list:
+ """The list of URIs for this source"""
+ try:
+ return self['Components'].split()
+ except KeyError:
+ return []
+
+ @components.setter
+ def components(self, components: list) -> None:
+ self['Components'] = ' '.join(components).strip()
+
+
+ @property
+ def options(self) -> dict:
+ """The options for this source"""
+ return self._options
+
+ @options.setter
+ def options(self, options:dict) -> None:
+ if 'Signed-By' in options:
+ self.signed_by = options['Signed-By']
+ if self.signed_by:
+ options.pop('Signed-By')
+ self._options = options
+
+ @property
+ def prefs(self):
+ """The path to any apt preferences files for this source."""
+ try:
+ prefs = self['X-Repolib-Prefs']
+ except KeyError:
+ prefs = ''
+
+ if prefs:
+ return Path(prefs)
+ return Path()
+
+ @prefs.setter
+ def prefs(self, prefs):
+ """Accept a str or a Path-like object"""
+ try:
+ del self['X-Repolib-Prefs']
+ except KeyError:
+ pass
+
+ if prefs:
+ prefs_str = str(prefs)
+ self['X-Repolib-Prefs'] = prefs_str
+
+
+ ## Option properties
+
+ @property
+ def architectures (self) -> str:
+ """architectures option"""
+ try:
+ return self['Architectures']
+ except KeyError:
+ return ''
+
+ @architectures.setter
+ def architectures(self, data) -> None:
+ try:
+ self.pop('Architectures')
+ except KeyError:
+ pass
+
+ if data:
+ self['Architectures'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def languages (self) -> str:
+ """languages option"""
+ try:
+ return self['Languages']
+ except KeyError:
+ return ''
+
+ @languages.setter
+ def languages(self, data) -> None:
+ try:
+ self.pop('Languages')
+ except KeyError:
+ pass
+
+ if data:
+ self['Languages'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def targets (self) -> str:
+ """targets option"""
+ try:
+ return self['Targets']
+ except KeyError:
+ return ''
+
+ @targets.setter
+ def targets(self, data) -> None:
+ try:
+ self.pop('Targets')
+ except KeyError:
+ pass
+
+ if data:
+ self['Targets'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def pdiffs (self) -> str:
+ """pdiffs option"""
+ try:
+ return self['Pdiffs']
+ except KeyError:
+ return ''
+
+ @pdiffs.setter
+ def pdiffs(self, data) -> None:
+ try:
+ self.pop('Pdiffs')
+ except KeyError:
+ pass
+
+ if data:
+ self['Pdiffs'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def by_hash (self) -> str:
+ """by_hash option"""
+ try:
+ return self['By-Hash']
+ except KeyError:
+ return ''
+
+ @by_hash.setter
+ def by_hash(self, data) -> None:
+ try:
+ self.pop('By-Hash')
+ except KeyError:
+ pass
+
+ if data:
+ self['By-Hash'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def allow_insecure (self) -> str:
+ """allow_insecure option"""
+ try:
+ return self['Allow-Insecure']
+ except KeyError:
+ return ''
+
+ @allow_insecure.setter
+ def allow_insecure(self, data) -> None:
+ try:
+ self.pop('Allow-Insecure')
+ except KeyError:
+ pass
+
+ if data:
+ self['Allow-Insecure'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def allow_weak (self) -> str:
+ """allow_weak option"""
+ try:
+ return self['Allow-Weak']
+ except KeyError:
+ return ''
+
+ @allow_weak.setter
+ def allow_weak(self, data) -> None:
+ try:
+ self.pop('Allow-Weak')
+ except KeyError:
+ pass
+
+ if data:
+ self['Allow-Weak'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def allow_downgrade_to_insecure (self) -> str:
+ """allow_downgrade_to_insecure option"""
+ try:
+ return self['Allow-Downgrade-To-Insecure']
+ except KeyError:
+ return ''
+
+ @allow_downgrade_to_insecure.setter
+ def allow_downgrade_to_insecure(self, data) -> None:
+ try:
+ self.pop('Allow-Downgrade-To-Insecure')
+ except KeyError:
+ pass
+
+ if data:
+ self['Allow-Downgrade-To-Insecure'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def trusted (self) -> str:
+ """trusted option"""
+ try:
+ return self['Trusted']
+ except KeyError:
+ return ''
+
+ @trusted.setter
+ def trusted(self, data) -> None:
+ try:
+ self.pop('Trusted')
+ except KeyError:
+ pass
+
+ if data:
+ self['Trusted'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def signed_by (self) -> str:
+ """signed_by option"""
+ try:
+ return self['Signed-By']
+ except KeyError:
+ return ''
+
+ @signed_by.setter
+ def signed_by(self, data) -> None:
+ try:
+ self.pop('Signed-By')
+ except KeyError:
+ pass
+
+ if data:
+ self['Signed-By'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def check_valid_until (self) -> str:
+ """check_valid_until option"""
+ try:
+ return self['Check-Valid-Until']
+ except KeyError:
+ return ''
+
+ @check_valid_until.setter
+ def check_valid_until(self, data) -> None:
+ try:
+ self.pop('Check-Valid-Until')
+ except KeyError:
+ pass
+
+ if data:
+ self['Check-Valid-Until'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def valid_until_min (self) -> str:
+ """valid_until_min option"""
+ try:
+ return self['Valid-Until-Min']
+ except KeyError:
+ return ''
+
+ @valid_until_min.setter
+ def valid_until_min(self, data) -> None:
+ try:
+ self.pop('Valid-Until-Min')
+ except KeyError:
+ pass
+
+ if data:
+ self['Valid-Until-Min'] = data
+ self._update_legacy_options()
+
+
+ @property
+ def valid_until_max (self) -> str:
+ """valid_until_max option"""
+ try:
+ return self['Valid-Until-Max']
+ except KeyError:
+ return ''
+
+ @valid_until_max.setter
+ def valid_until_max(self, data) -> None:
+ try:
+ self.pop('Valid-Until-Max')
+ except KeyError:
+ pass
+
+ if data:
+ self['Valid-Until-Max'] = data
+ self._update_legacy_options()
+
+ @property
+ def default_mirror(self) -> str:
+ """The default mirror/URI for the source"""
+ try:
+ return self['X-Repolib-Default-Mirror']
+ except KeyError:
+ return ''
+
+ @default_mirror.setter
+ def default_mirror(self, mirror) -> None:
+ if mirror:
+ self['X-Repolib-Default-Mirror'] = mirror
+ else:
+ self['X-Repolib-Default-Mirror'] = ''
+
+
+ ## Output Properties
+ @property
+ def deb822(self) -> str:
+ """The DEB822 representation of this source"""
+ self._update_legacy_options()
+ # comments get handled separately because they're a list, and list
+ # properties don't support .append()
+ if self.comments:
+ self['X-Repolib-Comments'] = '# '
+ self['X-Repolib-Comments'] += ' # '.join(self.comments)
+ _deb822 = self.dump()
+ if self.comments:
+ self.pop('X-Repolib-Comments')
+ if _deb822:
+ return _deb822
+ return ''
+
+ @property
+ def ui(self) -> str:
+ """The UI-friendly representation of this source"""
+ self._update_legacy_options()
+ _ui_list:list = self.deb822.split('\n')
+ ui_output: str = f'{self.ident}:\n'
+ for line in _ui_list:
+ key = line.split(':')[0]
+ if key not in util.output_skip_keys:
+ if line:
+ ui_output += f'{line}\n'
+ for key in util.keys_map:
+ ui_output = ui_output.replace(key, util.keys_map[key])
+ return ui_output
+
+ @property
+ def legacy(self) -> str:
+ """The legacy/one-line format representation of this source"""
+ self._update_legacy_options()
+
+ if str(self.prefs) != '.':
+ raise SourceError(
+ 'Apt Preferences files can only be used with DEB822-format sources.'
+ )
+
+ sourcecode = self.sourcecode_enabled
+ if len(self.types) > 1:
+ self.twin_source = True
+ self.types = [util.SourceType.BINARY]
+ sourcecode = True
+
+ legacy = ''
+
+ legacy += self._generate_legacy_output()
+ if self.twin_source:
+ legacy += '\n'
+ legacy += self._generate_legacy_output(sourcecode=True, enabled=sourcecode)
+
+ return legacy
+
+ def _generate_legacy_output(self, sourcecode=False, enabled=True) -> str:
+ """Generate a string of the current source in legacy format"""
+ legacy = ''
+
+ if len(self.types) > 1:
+ self.twin_source = True
+ self.types = [util.SourceType.BINARY]
+ for attr in ['types', 'uris', 'suites']:
+ if len(getattr(self, attr)) > 1:
+ msg = f'The source has too many {attr}.'
+ msg += f'Legacy-format sources support one {attr[:-1]} only.'
+ raise SourceError(msg)
+
+ if not self.enabled.get_bool() and not sourcecode:
+ legacy += '# '
+
+ if sourcecode and not enabled:
+ legacy += '# '
+
+ if sourcecode:
+ legacy += 'deb-src '
+ else:
+ legacy += self.types[0].value
+ legacy += ' '
+
+ options_string = self._legacy_options()
+ if options_string:
+ legacy += '['
+ legacy += options_string
+ legacy = legacy.strip()
+ legacy += '] '
+
+ legacy += f'{self.uris[0]} '
+ legacy += f'{self.suites[0]} '
+
+ for component in self.components:
+ legacy += f'{component} '
+
+ legacy += f' ## X-Repolib-Name: {self.name}'
+ legacy += f' # X-Repolib-ID: {self.ident}'
+ if self.comments:
+ for comment in self.comments:
+ legacy += f' # {comment}'
+
+ return legacy
+
+ def _legacy_options(self) -> str:
+ """Turn the current options into a oneline-style string
+
+ Returns: str
+ The one-line-format options string
+ """
+ options_str = ''
+ for key in self.options:
+ if self.options[key] != '':
+ options_str += f'{key}={self.options[key].replace(" ", ",")} '
+ return options_str
+
+ def _update_legacy_options(self) -> None:
+ """Updates the current set of legacy options"""
+ self.options = {
+ 'arch': self.architectures,
+ 'lang': self.languages,
+ 'target': self.targets,
+ 'pdiffs': self.pdiffs,
+ 'by-hash': self.by_hash,
+ 'allow-insecure': self.allow_insecure,
+ 'allow-weak': self.allow_weak,
+ 'allow-downgrade-to-insecure': self.allow_downgrade_to_insecure,
+ 'trusted': self.trusted,
+ 'signed-by': self.signed_by,
+ 'check-valid-until': self.check_valid_until,
+ 'valid-until-min': self.valid_until_min,
+ 'valid-until-max': self.valid_until_max,
+ }
\ No newline at end of file
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/system.py b/repolib/usr/lib/python3/dist-packages/repolib/system.py
new file mode 100644
index 0000000..b22ef09
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/system.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import logging
+
+from pathlib import Path
+
+from . import util
+from .file import SourceFile
+from .source import Source
+from .shortcuts import popdev, ppa
+
+
+log = logging.getLogger(__name__)
+
+def load_all_sources() -> None:
+ """Loads all of the sources present on the system."""
+ log.info('Loading all sources')
+
+ util.sources.clear()
+ util.files.clear()
+ util.keys.clear()
+ util.errors.clear()
+
+ sources_path = Path(util.SOURCES_DIR)
+ sources_files = sources_path.glob('*.sources')
+ legacy_files = sources_path.glob('*.list')
+
+ for file in sources_files:
+ try:
+ sourcefile = SourceFile(name=file.stem)
+ log.debug('Loading %s', file)
+ sourcefile.load()
+ if file.name not in util.files:
+ util.files[file.name] = sourcefile
+
+ except Exception as err:
+ util.errors[file.name] = err
+
+ for file in legacy_files:
+ try:
+ sourcefile = SourceFile(name=file.stem)
+ sourcefile.load()
+ util.files[file.name] = sourcefile
+ except Exception as err:
+ util.errors[file.name] = err
+
+ for f in util.files:
+ file = util.files[f]
+ for source in file.sources:
+ if source.ident in util.sources:
+ source.ident = f'{file.name}-{source.ident}'
+ source.file.save()
+ util.sources[source.ident] = source
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/__init__.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_key.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_key.py
new file mode 100644
index 0000000..977324b
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_key.py
@@ -0,0 +1,142 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import unittest
+
+from ..key import SourceKey
+from .. import util, system
+from .. import set_testing
+
+# System76 Signing PubKey, for test import
+KEY_DATA = (
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFlL+3MBEADdNM9Xy2t3EtKU1i3R1o'
+ '1OCgJqLiDm8OZZq47InYID8oAPKRjd\n0UDVJTrvfsB4oJH97VRi2hGv2xmc19OaFE/NsQBZW/'
+ '7/3ypLr8eyaNgvscsmG/WN\ncM1cbMZtwd1b0JOr3bNTzp6WKRI3jo9uRw7duM8FwPjKm76Lbo'
+ 'DQbAR+4Szm3O8x\n/om8Gs1MRPUkY2dVz5KzednFLHwy7qnUXR3WRB5K1L9EBZkFDDNqnyViUI'
+ 'rE4bTm\nBC9mTg/Xfw/QXUFYz3t/YTYduAU0o1q2yei+8tVAJKh7H9t3PrQ95l3RUUcaAvba\n'
+ 'A9zlCrI8fonpxu7eSpkqzT4uCkfxdLVwittl1DumKTEkSXDQ5txY21igbSZZQwBA\nZf9MnFhJ'
+ 'fPsEIq2YHRc1FBcQxiAIpnGizv7FgYY5FxmZQ7592dMQOZ00h+lDSQug\nNMxloHCogaXR038u'
+ 'IKGTQnQEVcT46FtTRkLMSvbigy+RVSchdu9MEBBPgD3vSv53\nNEobXsLiZ9hF6Hk7XI2WxP5j'
+ '1zWTPmzxvf9NDOWz2Sw9Z+ilf252LXoxZQaMngp8\nXL32uvw7q+mjB6F1W/qpe3b32uu7eGNr'
+ 'DWJ5veE808hpXXj803TllmRUfMGUrtY9\nk7uUTQQWtrJ5uZ0QmsTk1oJHCPIUjjuiNtQfq28+'
+ 'bfg8FEJ/F1N1mB0IvwARAQAB\ntCxQb3AgT1MgKElTTyBTaWduaW5nIEtleSkgPGluZm9Ac3lz'
+ 'dGVtNzYuY29tPokC\nNwQTAQIAIgUCWUv7cwIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AA'
+ 'CgkQIE3Y\nrsM6ev8kXw/4p/8fOH8wM59ZoU0t1+fv3O8dYaDdTVTVIEno9snrsx5A5tbMu59r'
+ '\nHoBaxGenv/PB0l8yANhRX+HVmU/l0Sj0lTlEkYzgH/IT2Ne60s1ETgI7DlgSuYyP\nH8wq61'
+ '85+2DyE2+R/XcXGq0I++QUq1Y6rS+B4KIyYcgpJotcVNFaZiwuJZE31uLg\nkVMZrm1oObHear'
+ '7P2JQTbgsENMZDJEhQBCGKVdnAfVdKUeUrd07syr0cDe3kwY9o\ncNc00bhIh23cLTJE2omok9'
+ 'yCsXoeFJlPMyZw8WvEa5oaYWzP4Yw7nF8/27JTzZ70\nDjK2D2xoTkr0cP87LtZulS6FC3lxLu'
+ 'Z6hSaxsqoBH8Dd1uyYVEzLDsIRMtSHsXk+\n3kLrr1p7/7/vjGShlYkbLtP4jWnlHc6vSxIzm/'
+ 'MQmQMCfjeo3QH7GGw88mYtXngQ\n/Zna6wz0oL6pGM/4t90SCxTxRqCnoxMxzkcpt9n42bj79g'
+ 'rESOMH4wm3ExfuPk7I\nDtY+SqzIq0QvoPbC3XJLusWVgwUsRF2FpTTRTHEiWEMjWDKDVEyT4K'
+ '1k1k3f/gi2\n6LdtXwqDwzUvJJU5HYwVFywt+0jt5F0ZlTlPizz3iHw4gMLOielRShl+gZrU2U'
+ '0O\naj1Hyts9LymEKMUvRQGwMqCZcXo6sGjs59tTsfeGX16PTOyBri8eoLkCDQRZS/tz\nARAA'
+ 'pD9IWm4zS1AuBcOTuvh1E/ciKHGIUtW3JftD9ah8loEeckakgG5Xn9he1X6J\nyxPULpsptcCC'
+ 'cKXlw853ZQK9PLQJX6moWLH+qf2Zo3UAn/YEsWk+KsHoxPXHNUds\nu/j6UKkqEk8c7H92hUo8'
+ 'aWghO3p4HDVJ9KmGtueQ3jOv8Qun7Eh9cIo0A59cKmMv\njKUiYHLIJw8bkveQ8rVPul1ZHn56'
+ 'ORiBi58vm3tzjI4UWHQMjiKxXT6H5eG/f5K6\nuaK8lljh6n6jhdnQCpBcdtSIbhE/6YRv2+Ig'
+ 'L+BRssvprBtx4/sBwKjNNqzWPeGy\nUDHMiF88ETYqZ8DfukQ/e5XuaxjU41g/F8cw8BeVTBMv'
+ 'eb1YTyOoWcWvTL+hoBfS\nqYc/lvDHmmJ7/IgeMvUE6KoByP4ub5wX52mJTqgMC4GMhA04BC60'
+ 'B+NfVAXLh2pa\nTRJAHoWTDswOxbR6q9zPEFGZzV04B9Y96EavwMpT5IzG2fOPBwvdT0EDnt+v'
+ 'Q/iB\nc9O7CvkRTROAV+RoNCLY2XU8yNc/XxuI66PCE4Q96jW4uDzHvi6sPW/glsfRi2NT\nRW'
+ 'CO15KMVf0aypXeBpSbHIXIYGdXRQRpw980IW6PrElPpqZ5/DGbkXei5CuruF2R\nmltuu3MqYQ'
+ 'jcUvP9T7s0e5GAFgQFrR/8q29nVULq8IF4vzUAEQEAAYkCHwQYAQIA\nCQUCWUv7cwIbDAAKCR'
+ 'AgTdiuwzp6/wTGD/9Co4gEmTTOW++FneMMJo5K4WqeWVRg\ng1q5+yoVqgWq3k6lLsEC5kxR30'
+ '5BAAcvXo9XPKdo62ySYmhIFOpIz/TkeTUxDZaw\nsLtcBxXUME2L5j/1od1V9lxecUvLAgA11o'
+ '5Kb8TMKn5ZcmGhadtTLslWQcYsKqhw\nLaYQRlcxLDHYT8DXFkHgDhUMMbpt07dU5v5lIjgtGN'
+ 'HRhdS7/lCmSWOBtYapwpAH\nGYSmahN0zO36VHzOB5uwFue0tSoQiBEvLrCV/8ZtT2S5NkXaSm'
+ 'isz6B5Vr6DRtWI\nOamW5pMbSL8WQNQ99Kik05ctERjv2NgxI4JQo/a4KKthRrT4JlixXmrfJD'
+ 'uPyDPp\nRuTu1Elo6snoqWKQNf1sEPKvcv7EviNxBOhbTKivWrJXMnbOme7+UlNLcq7VAFp3\n'
+ 'x5hxk/ap0WqH/hs7+8jMBC8nS402MoM7EyLS0++kbOuEL/Prf3+JxFRqIu5Df77J\n+bUmTtKI'
+ 'CV43ikiVWmnP5OuJj2JPSOTR+rLxAQYpyHmo7HKXE63FbH1FVLgsT88+\nEW6VtI01I7EYmKQX'
+ 'EqQo52yfeHKDrQjGNVBWMKcXj0SVU+QQ1Ue/4yLwA+74VD2d\nfOyJI22NfTI+3SMAsMQ8L+WV'
+ 'QI+58bu7+iEqoEfHCXikE8BtTbJAN4Oob1lrjfOe\n5utH/lMP9suRWw==\n=NL3f\n-----EN'
+ 'D PGP PUBLIC KEY BLOCK-----\n'
+)
+
+class KeyTestCase(unittest.TestCase):
+ def setUp(self):
+ set_testing()
+ self.key_data = KEY_DATA
+ self.keys_dir = util.KEYS_DIR
+ self.key_id = '204DD8AEC33A7AFF'
+ self.key_uids = ['Pop OS (ISO Signing Key) ']
+ self.key_length = '4096'
+ self.key_date = '1498151795'
+
+ def test_import_ascii(self):
+ key = SourceKey(name='popdev')
+ key.load_key_data(ascii=self.key_data)
+ key_dict = key.gpg.list_keys()[0]
+ key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
+
+ self.assertEqual(key.path, key_path)
+ self.assertEqual(len(key.gpg.list_keys()), 1)
+ self.assertEqual(key_dict['keyid'], self.key_id)
+ self.assertEqual(key_dict['uids'], self.key_uids)
+ self.assertEqual(key_dict['length'], self.key_length)
+ self.assertEqual(key_dict['date'], self.key_date)
+
+ def test_key_save_load(self):
+ print(self.keys_dir)
+ key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
+ if key_path.exists():
+ key_path.unlink()
+
+ self.assertFalse(key_path.exists())
+ key_save = SourceKey(name='popdev')
+ key_save.load_key_data(ascii=self.key_data)
+ key_save.save_gpg()
+
+ self.assertTrue(key_save.path.exists())
+
+ key_load = SourceKey()
+ key_load.reset_path(name='popdev')
+ key_dict = key_load.gpg.list_keys()[0]
+
+ self.assertEqual(key_load.path, key_path)
+ self.assertEqual(len(key_load.gpg.list_keys()), 1)
+ self.assertEqual(key_dict['keyid'], self.key_id)
+ self.assertEqual(key_dict['uids'], self.key_uids)
+ self.assertEqual(key_dict['length'], self.key_length)
+ self.assertEqual(key_dict['date'], self.key_date)
+
+ def test_delete_key(self):
+ key_path = self.keys_dir / 'popdev-archive-keyring.gpg'
+ if key_path.exists():
+ key_path.unlink()
+
+ self.assertFalse(key_path.exists())
+
+ self.assertFalse(key_path.exists())
+ key_save = SourceKey(name='popdev')
+ key_save.load_key_data(ascii=self.key_data)
+
+ key_save.save_gpg()
+
+ self.assertTrue(key_save.path.exists())
+
+ key_load = SourceKey()
+ key_load.reset_path(name='popdev')
+ key_load.delete_key()
+
+ self.assertFalse(key_load.path.exists())
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_parsedeb.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_parsedeb.py
new file mode 100644
index 0000000..0a975a0
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_parsedeb.py
@@ -0,0 +1,188 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+
+This is a library for parsing deb lines into deb822-format data.
+"""
+
+import unittest
+
+from ..source import Source
+from .. import util
+
+class DebTestCase(unittest.TestCase):
+ def test_normal_source(self):
+ source = Source()
+ source.load_from_data([
+ 'deb http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.types, [util.SourceType.BINARY])
+ self.assertTrue(source.enabled.get_bool())
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.suites, ['suite'])
+ self.assertEqual(source.components, ['main'])
+ self.assertEqual(source.ident, 'example-com-binary')
+
+ def test_source_with_multiple_components(self):
+ source = Source()
+ source.load_from_data([
+ 'deb http://example.com/ suite main nonfree'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.suites, ['suite'])
+ self.assertEqual(source.components, ['main', 'nonfree'])
+
+ def test_source_with_option(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64 ] http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.architectures, 'amd64')
+
+ def test_source_uri_with_brackets(self):
+ source = Source()
+ source.load_from_data([
+ 'deb http://example.com/[release]/ubuntu suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/[release]/ubuntu'])
+
+ def test_source_options_with_colons(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=arm:2 ] http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.architectures, 'arm:2')
+
+ def test_source_with_multiple_option_values(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=armel,amd64 ] http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.architectures, 'armel amd64')
+
+ def test_source_with_multiple_options(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64 lang=en_US ] http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.architectures, 'amd64')
+ self.assertEqual(source.languages, 'en_US')
+
+ def test_source_with_multiple_options_with_multiple_values(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64,armel lang=en_US,en_CA ] '
+ 'http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example.com/'])
+ self.assertEqual(source.architectures, 'amd64 armel')
+ self.assertEqual(source.languages, 'en_US en_CA')
+
+ def test_source_uri_with_brackets_and_options(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64 lang=en_US,en_CA ] '
+ 'http://example][.com/[release]/ubuntu suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
+ self.assertEqual(source.architectures, 'amd64')
+ self.assertEqual(source.languages, 'en_US en_CA')
+
+ def test_source_uri_with_brackets_and_options_with_colons(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64,arm:2 lang=en_US,en_CA ] '
+ 'http://example][.com/[release]/ubuntu suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example][.com/[release]/ubuntu'])
+ self.assertEqual(source.architectures, 'amd64 arm:2')
+ self.assertEqual(source.languages, 'en_US en_CA')
+
+ def test_worst_case_sourcenario(self):
+ source = Source()
+ source.load_from_data([
+ 'deb [ arch=amd64,arm:2,arm][ lang=en_US,en_CA ] '
+ 'http://example][.com/[release:good]/ubuntu suite main restricted '
+ 'nonfree not-a-component'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.uris, ['http://example][.com/[release:good]/ubuntu'])
+ self.assertEqual(source.suites, ['suite'])
+ self.assertEqual(source.components, [
+ 'main', 'restricted', 'nonfree', 'not-a-component'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.architectures, 'amd64 arm:2 arm][')
+ self.assertEqual(source.languages, 'en_US en_CA')
+
+ def test_source_code_source(self):
+ source = Source()
+ source.load_from_data([
+ 'deb-src http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.types, [util.SourceType.SOURCECODE])
+
+ def test_disabled_source(self):
+ source = Source()
+ source.load_from_data([
+ '# deb http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertFalse(source.enabled.get_bool())
+
+ def test_disabled_source_without_space(self):
+ source = Source()
+ source.load_from_data([
+ '#deb http://example.com/ suite main'
+ ])
+ source.generate_default_ident()
+ self.assertFalse(source.enabled.get_bool())
+
+ def test_source_with_trailing_comment(self):
+ source = Source()
+ source.load_from_data([
+ 'deb http://example.com/ suite main # This is a comment'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.suites, ['suite'])
+ self.assertEqual(source.components, ['main'])
+
+ def test_disabled_source_with_trailing_comment(self):
+ source = Source()
+ source.load_from_data([
+ '# deb http://example.com/ suite main # This is a comment'
+ ])
+ source.generate_default_ident()
+ self.assertEqual(source.suites, ['suite'])
+ self.assertEqual(source.components, ['main'])
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_popdev.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_popdev.py
new file mode 100644
index 0000000..8207b09
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_popdev.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import unittest
+
+from ..shortcuts import popdev
+from .. import util
+
+class PopdevTestCase(unittest.TestCase):
+
+ def test_ppa(self):
+ source = popdev.PopdevSource()
+
+ # Verification data
+ uris_test = ['http://apt.pop-os.org/staging/master']
+ signed_test = '/usr/share/keyrings/popdev-archive-keyring.gpg'
+ source.load_from_shortcut(shortcut='popdev:master')
+
+ self.assertEqual(source.uris, uris_test)
+ self.assertEqual(source.ident, 'popdev-master')
+ self.assertEqual(source.suites, [util.DISTRO_CODENAME])
+ self.assertEqual(source.components, ['main'])
+ self.assertEqual(source.types, [util.SourceType.BINARY])
+ self.assertTrue(source.signed_by.endswith(signed_test))
\ No newline at end of file
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_ppa.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_ppa.py
new file mode 100644
index 0000000..9b346cf
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_ppa.py
@@ -0,0 +1,42 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import unittest
+
+from ..shortcuts import ppa
+from .. import util
+
+class PPATestCase(unittest.TestCase):
+
+ def test_ppa(self):
+ source = ppa.PPASource()
+
+ # Verification data
+ uris_test = ['http://ppa.launchpad.net/system76/pop/ubuntu']
+ signed_test = '/usr/share/keyrings/ppa-system76-pop-archive-keyring.gpg'
+ source.load_from_shortcut(shortcut='ppa:system76/pop', meta=False, key=False)
+
+ self.assertEqual(source.uris, uris_test)
+ self.assertEqual(source.ident, 'ppa-system76-pop')
+ self.assertEqual(source.suites, [util.DISTRO_CODENAME])
+ self.assertEqual(source.components, ['main'])
+ self.assertEqual(source.types, [util.SourceType.BINARY])
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_source.py b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_source.py
new file mode 100644
index 0000000..ae7f38e
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/unittest/test_source.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import unittest
+
+from .. import file, util, source
+
+class SourceTestCase(unittest.TestCase):
+ def setUp(self):
+ util.set_testing()
+ self.source = source.Source()
+ self.source.ident = 'test'
+ self.source.name = 'Test Source'
+ self.source.enabled = True
+ self.source.types = [util.SourceType.BINARY, util.SourceType.SOURCECODE]
+ self.source.uris = ['http://example.com/ubuntu', 'http://example.com/mirror']
+ self.source.suites = ['suite', 'suite-updates']
+ self.source.components = ['main', 'contrib', 'nonfree']
+ self.source.architectures = 'amd64 armel'
+ self.source.languages = 'en_US en_CA'
+ self.file = file.SourceFile(name=self.source.ident)
+ self.file.add_source(self.source)
+ self.source.file = self.file
+
+ self.source_legacy = source.Source()
+ self.source_legacy.ident = 'test-legacy'
+ self.source_legacy.name = 'Test Legacy Source'
+ self.source_legacy.enabled = True
+ self.source_legacy.types = [util.SourceType.BINARY]
+ self.source_legacy.uris = ['http://example.com/ubuntu']
+ self.source_legacy.suites = ['suite']
+ self.source_legacy.components = ['main', 'contrib', 'nonfree']
+ self.source_legacy.architectures = 'amd64 armel'
+ self.source_legacy.languages = 'en_US en_CA'
+ self.source_legacy.file = file.SourceFile(name=self.source_legacy.ident)
+ self.source_legacy.file.format = util.SourceFormat.LEGACY
+
+
+ def test_default_source_data(self):
+ self.assertEqual(self.source.name, 'Test Source')
+ self.assertTrue(self.source.enabled.get_bool())
+ self.assertEqual(
+ self.source.types,
+ [util.SourceType.BINARY, util.SourceType.SOURCECODE]
+ )
+ self.assertTrue(self.source.sourcecode_enabled)
+ self.assertEqual(
+ self.source.uris,
+ ['http://example.com/ubuntu', 'http://example.com/mirror']
+ )
+ self.assertEqual(
+ self.source.suites,
+ ['suite', 'suite-updates']
+ )
+ self.assertEqual(
+ self.source.components,
+ ['main', 'contrib', 'nonfree']
+ )
+ self.assertEqual(self.source.architectures, 'amd64 armel')
+ self.assertEqual(self.source.languages, 'en_US en_CA')
+ self.assertEqual(self.source.file.path.name, 'test.sources')
+
+ def test_output_822(self):
+ source_string = (
+ 'X-Repolib-ID: test\n'
+ 'X-Repolib-Name: Test Source\n'
+ 'Enabled: yes\n'
+ 'Types: deb deb-src\n'
+ 'URIs: http://example.com/ubuntu http://example.com/mirror\n'
+ 'Suites: suite suite-updates\n'
+ 'Components: main contrib nonfree\n'
+ 'Architectures: amd64 armel\n'
+ 'Languages: en_US en_CA\n'
+ )
+ legacy_source_string = (
+ 'X-Repolib-ID: test-legacy\n'
+ 'X-Repolib-Name: Test Legacy Source\n'
+ 'Enabled: yes\n'
+ 'Types: deb\n'
+ 'URIs: http://example.com/ubuntu\n'
+ 'Suites: suite\n'
+ 'Components: main contrib nonfree\n'
+ 'Architectures: amd64 armel\n'
+ 'Languages: en_US en_CA\n'
+ )
+ self.assertEqual(self.source.deb822, source_string)
+ self.assertEqual(self.source_legacy.deb822, legacy_source_string)
+
+ def test_output_ui(self):
+ source_string = (
+ 'test:\n'
+ 'Name: Test Source\n'
+ 'Enabled: yes\n'
+ 'Types: deb deb-src\n'
+ 'URIs: http://example.com/ubuntu http://example.com/mirror\n'
+ 'Suites: suite suite-updates\n'
+ 'Components: main contrib nonfree\n'
+ 'Architectures: amd64 armel\n'
+ 'Languages: en_US en_CA\n'
+ ''
+ )
+ legacy_source_string = (
+ 'test-legacy:\n'
+ 'Name: Test Legacy Source\n'
+ 'Enabled: yes\n'
+ 'Types: deb\n'
+ 'URIs: http://example.com/ubuntu\n'
+ 'Suites: suite\n'
+ 'Components: main contrib nonfree\n'
+ 'Architectures: amd64 armel\n'
+ 'Languages: en_US en_CA\n'
+ )
+ self.assertEqual(self.source.ui, source_string)
+ self.assertEqual(self.source_legacy.ui, legacy_source_string)
+
+ def test_output_legacy(self):
+ source_string = (
+ 'deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source # X-Repolib-ID: test-legacy'
+ )
+ self.assertEqual(self.source_legacy.legacy, source_string)
+
+ def test_enabled(self):
+ self.source.enabled = False
+ self.assertFalse(self.source.enabled.get_bool())
+
+ def test_sourcecode_enabled(self):
+ self.source.sourcecode_enabled = False
+ self.assertEqual(self.source.types, [util.SourceType.BINARY])
+
+ def test_dict_access(self):
+ self.assertEqual(self.source['X-Repolib-ID'], 'test')
+ self.assertEqual(self.source['X-Repolib-Name'], 'Test Source')
+ self.assertEqual(self.source['Enabled'], 'yes')
+ self.assertEqual(self.source['Enabled'], 'yes')
+ self.assertEqual(self.source['Types'], 'deb deb-src')
+ self.assertEqual(self.source['URIs'], 'http://example.com/ubuntu http://example.com/mirror')
+ self.assertEqual(self.source['Suites'], 'suite suite-updates')
+ self.assertEqual(self.source['Components'], 'main contrib nonfree')
+ self.assertEqual(self.source['Architectures'], 'amd64 armel')
+ self.assertEqual(self.source['Languages'], 'en_US en_CA')
+
+ def test_load(self):
+ load_source = source.Source()
+ load_source.load_from_data([
+ 'X-Repolib-ID: load-test',
+ 'X-Repolib-Name: Test Source Loading',
+ 'Enabled: yes',
+ 'Types: deb',
+ 'URIs: http://example.com/ubuntu http://example.com/mirror',
+ 'Suites: suite suite-updates',
+ 'Components: main contrib nonfree',
+ 'Architectures: amd64 armel',
+ 'Languages: en_US en_CA',
+ ])
+
+ self.assertEqual(load_source.ident, 'load-test')
+ self.assertEqual(load_source.name, 'Test Source Loading')
+ self.assertTrue(load_source.enabled.get_bool())
+ self.assertEqual(
+ load_source.types,
+ [util.SourceType.BINARY]
+ )
+ self.assertEqual(
+ load_source.uris,
+ ['http://example.com/ubuntu', 'http://example.com/mirror']
+ )
+ self.assertEqual(
+ load_source.suites,
+ ['suite', 'suite-updates']
+ )
+ self.assertEqual(
+ load_source.components,
+ ['main', 'contrib', 'nonfree']
+ )
+ self.assertEqual(load_source.architectures, 'amd64 armel')
+ self.assertEqual(load_source.languages, 'en_US en_CA')
+
+ load_legacy_source = source.Source()
+ load_legacy_source.load_from_data(
+ ['deb [arch=amd64,armel lang=en_US,en_CA] http://example.com/ubuntu suite main contrib nonfree ## X-Repolib-Name: Test Legacy Source Loading # X-Repolib-ID: test-load-legacy']
+ )
+
+ self.assertEqual(load_legacy_source.ident, 'test-load-legacy')
+ self.assertEqual(load_legacy_source.name, 'Test Legacy Source Loading')
+ self.assertTrue(load_legacy_source.enabled.get_bool())
+ self.assertEqual(
+ load_legacy_source.types,
+ [util.SourceType.BINARY]
+ )
+ self.assertEqual(
+ load_legacy_source.uris,
+ ['http://example.com/ubuntu']
+ )
+ self.assertEqual(
+ load_legacy_source.suites,
+ ['suite']
+ )
+ self.assertEqual(
+ load_legacy_source.components,
+ ['main', 'contrib', 'nonfree']
+ )
+ self.assertEqual(load_legacy_source.architectures, 'amd64 armel')
+ self.assertEqual(load_legacy_source.languages, 'en_US en_CA')
+
+ def test_save_load(self):
+ self.source.file.save()
+ load_source_file = file.SourceFile(name='test')
+ load_source_file.load()
+ self.assertGreater(len(load_source_file.sources), 0)
+ self.assertGreater(
+ len(load_source_file.contents), len(load_source_file.sources)
+ )
+ load_source = load_source_file.sources[0]
+
+ self.assertEqual(load_source.ident, self.source.ident)
+ self.assertEqual(load_source.name, self.source.name)
+ self.assertEqual(load_source.enabled, self.source.enabled)
+ self.assertEqual(load_source.types, self.source.types)
+ self.assertEqual(load_source.sourcecode_enabled, self.source.sourcecode_enabled)
+ self.assertEqual(load_source.uris, self.source.uris)
+ self.assertEqual(load_source.suites, self.source.suites)
+ self.assertEqual(load_source.components, self.source.components)
+ self.assertEqual(load_source.architectures, self.source.architectures)
+ self.assertEqual(load_source.languages, self.source.languages)
+ self.assertEqual(load_source.file.name, self.source.file.name)
+
\ No newline at end of file
diff --git a/repolib/usr/lib/python3/dist-packages/repolib/util.py b/repolib/usr/lib/python3/dist-packages/repolib/util.py
new file mode 100644
index 0000000..cb303a9
--- /dev/null
+++ b/repolib/usr/lib/python3/dist-packages/repolib/util.py
@@ -0,0 +1,455 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2022, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+
+import atexit
+import logging
+import re
+import tempfile
+
+from enum import Enum
+from pathlib import Path
+from urllib.parse import urlparse
+from urllib import request, error
+
+import dbus
+
+SOURCES_DIR = Path('/etc/apt/sources.list.d')
+KEYS_DIR = Path('/etc/apt/keyrings/')
+TESTING = False
+KEYSERVER_QUERY_URL = 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x'
+
+log = logging.getLogger(__name__)
+
+class RepoError(Exception):
+ """ Exception from this module."""
+
+ def __init__(self, *args, code=1, **kwargs):
+ """Exception with a source object
+
+ Arguments:
+ code (:obj:`int`, optional, default=1): Exception error code.
+ """
+ super().__init__(*args, **kwargs)
+ self.code = code
+
+try:
+ import distro
+ DISTRO_CODENAME = distro.codename()
+except ImportError:
+ DISTRO_CODENAME = 'linux'
+
+class SourceFormat(Enum):
+ """Enum of SourceFile Formats"""
+ DEFAULT = "sources"
+ LEGACY = "list"
+
+class SourceType(Enum):
+ """Enum of repository types"""
+ BINARY = 'deb'
+ SOURCECODE = 'deb-src'
+
+ def ident(self) -> str:
+ """Used for getting a version of the format for idents"""
+ ident = f'{self.value}'
+ ident = ident.replace('deb-src', 'source')
+ ident = ident.replace('deb', 'binary')
+ return ident
+
+class AptSourceEnabled(Enum):
+ """ Helper Enum to translate between bool data and the Deb822 format. """
+ TRUE = 'yes'
+ FALSE = 'no'
+
+ def get_bool(self):
+ """ Return a bool based on the value. """
+ # pylint: disable=comparison-with-callable
+ # This doesnt seem to actually be a callable in this case.
+ if self.value == "yes":
+ return True
+
+ return False
+
+valid_keys = [
+ 'X-Repolib-Name:',
+ 'X-Repolib-ID:',
+ 'X-Repolib-Default-Mirror:',
+ 'X-Repolib-Comment',
+ 'X-Repolib-Prefs',
+ 'Enabled:',
+ 'Types:',
+ 'URIs:',
+ 'Suites:',
+ 'Components:',
+ 'Architectures:',
+ 'Languages:',
+ 'Targets:',
+ 'PDiffs:',
+ 'By-Hash:',
+ 'Allow-Insecure:',
+ 'Allow-Weak:',
+ 'Allow-Downgrade-To-Insecure:',
+ 'Trusted:',
+ 'Signed-By:',
+ 'Check-Valid-Until:',
+ 'Valid-Until-Min:',
+ 'Valid-Until-Max:',
+]
+
+output_skip_keys = [
+ 'X-Repolib-Prefs',
+ 'X-Repolib-ID',
+]
+
+options_inmap = {
+ 'arch': 'Architectures',
+ 'lang': 'Languages',
+ 'target': 'Targets',
+ 'pdiffs': 'PDiffs',
+ 'by-hash': 'By-Hash',
+ 'allow-insecure': 'Allow-Insecure',
+ 'allow-weak': 'Allow-Weak',
+ 'allow-downgrade-to-insecure': 'Allow-Downgrade-To-Insecure',
+ 'trusted': 'Trusted',
+ 'signed-by': 'Signed-By',
+ 'check-valid-until': 'Check-Valid-Until',
+ 'valid-until-min': 'Valid-Until-Min',
+ 'valid-until-max': 'Valid-Until-Max'
+}
+
+options_outmap = {
+ 'Architectures': 'arch',
+ 'Languages': 'lang',
+ 'Targets': 'target',
+ 'PDiffs': 'pdiffs',
+ 'By-Hash': 'by-hash',
+ 'Allow-Insecure': 'allow-insecure',
+ 'Allow-Weak': 'allow-weak',
+ 'Allow-Downgrade-To-Insecure': 'allow-downgrade-to-insecure',
+ 'Trusted': 'trusted',
+ 'Signed-By': 'signed-by',
+ 'Check-Valid-Until': 'check-valid-until',
+ 'Valid-Until-Min': 'valid-until-min',
+ 'Valid-Until-Max': 'valid-until-max'
+}
+
+true_values = [
+ True,
+ 'True',
+ 'true',
+ 'Yes',
+ 'yes',
+ 'YES',
+ 'y',
+ 'Y',
+ AptSourceEnabled.TRUE,
+ 1
+]
+
+keys_map = {
+ 'X-Repolib-Name: ': 'Name: ',
+ 'X-Repolib-ID: ': 'Ident: ',
+ 'X-Repolib-Comments: ': 'Comments: ',
+ 'X-Repolib-Default-Mirror: ': 'Default Mirror: ',
+}
+
+PRETTY_PRINT = '\n '
+
+_KEYS_TEMPDIR = tempfile.TemporaryDirectory()
+TEMP_DIR = Path(_KEYS_TEMPDIR.name)
+
+options_re = re.compile(r'[^@.+]\[([^[]+.+)\]\ ')
+uri_re = re.compile(r'\w+:(\/?\/?)[^\s]+')
+
+CLEAN_CHARS = {
+ 33: None,
+ 64: 45,
+ 35: 45,
+ 36: 45,
+ 37: 45,
+ 94: 45,
+ 38: 45,
+ 42: 45,
+ 41: None,
+ 40: None,
+ 43: 45,
+ 61: 45,
+ 91: None,
+ 92: None,
+ 93: None,
+ 123: None,
+ 125: None,
+ 124: 95,
+ 63: None,
+ 47: 95,
+ 46: 45,
+ 60: 95,
+ 62: 95,
+ 44: 95,
+ 96: None,
+ 126: None,
+ 32: 95,
+ 58: None,
+ 59: None,
+}
+
+sources:dict = {}
+files:dict = {}
+keys:dict = {}
+errors:dict = {}
+
+
+def scrub_filename(name: str = '') -> str:
+ """ Clean up a string intended for a filename.
+
+ Arguments:
+ name (str): The prospective name to scrub.
+
+ Returns: str
+ The cleaned-up name.
+ """
+ return name.translate(CLEAN_CHARS)
+
+def set_testing(testing:bool=True) -> None:
+ """Sets Repolib in testing mode where changes will not be saved.
+
+ Arguments:
+ testing(bool): Whether testing mode should be enabled or disabled
+ (Defaul: True)
+ """
+ global KEYS_DIR
+ global SOURCES_DIR
+
+ testing_tempdir = tempfile.TemporaryDirectory()
+
+ if not testing:
+ KEYS_DIR = '/usr/share/keyrings'
+ SOURCES_DIR = '/etc/apt/sources.list.d'
+ return
+
+ testing_root = Path(testing_tempdir.name)
+ KEYS_DIR = testing_root / 'usr' / 'share' / 'keyrings'
+ SOURCES_DIR = testing_root / 'etc' / 'apt' / 'sources.list.d'
+
+
+def _cleanup_temsps() -> None:
+ """Clean up our tempdir"""
+ _KEYS_TEMPDIR.cleanup()
+ # _TESTING_TEMPDIR.cleanup()
+
+atexit.register(_cleanup_temsps)
+
+def dbus_quit():
+ bus = dbus.SystemBus()
+ privileged_object = bus.get_object('org.pop_os.repolib', '/Repo')
+ privileged_object.exit()
+
+def compare_sources(source1, source2, excl_keys:list) -> bool:
+ """Compare two sources based on arbitrary criteria.
+
+ This looks at a given list of keys, and if the given keys between the two
+ given sources are identical, returns True.
+
+ Arguments:
+ source1, source2(Source): The two sources to compare
+ excl_keys([str]): Any keys to exclude from the comparison
+
+ Returns: bool
+ `True` if the sources are identical, otherwise `False`.
+ """
+ for key in source1:
+ if key in excl_keys:
+ continue
+ if key in source2:
+ if source1[key] != source2[key]:
+ return False
+ else:
+ continue
+ else:
+ return False
+ for key in source2:
+ if key in excl_keys:
+ continue
+ if key in source1:
+ if source1[key] != source2[key]:
+ return False
+ else:
+ continue
+ else:
+ return False
+ return True
+
+def find_differences_sources(source1, source2, excl_keys:list) -> dict:
+ """Find key-value pairs which differ between two sources.
+
+ Arguments:
+ source1, source2(Source): The two sources to compare
+ excl_keys([str]): Any keys to exclude from the comparison
+
+ Returns: dict{'key': ('source1[key]','source2[key]')}
+ The dictionary of different keys, with the key values from each source.
+ """
+ differing_keys:dict = {}
+
+ for key in source1:
+ if key in excl_keys:
+ continue
+ if key in source2:
+ if source1[key] == source2[key]:
+ continue
+ differing_keys[key] = (source1[key], source2[key])
+ differing_keys[key] = (source1[key], '')
+ for key in source2:
+ if key in excl_keys:
+ continue
+ if key in source1:
+ if source1[key] == source2[key]:
+ continue
+ differing_keys[key] = ('', source2[key])
+
+ return differing_keys
+
+def combine_sources(source1, source2) -> None:
+ """Combine the data in two sources into one.
+
+ Arguments:
+ source1(Source): The source to be merged into
+ source2(Source): The source to merge from
+ """
+ for key in source1:
+ if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
+ continue
+ if key in source2:
+ source1[key] += f' {source2[key]}'
+ for key in source2:
+ if key in ('X-Repolib-Name', 'X-Repolib-ID', 'Enabled', 'Types'):
+ continue
+ if key in source1:
+ source1[key] += f' {source2[key]}'
+
+ # Need to deduplicate the list
+ for key in source1:
+ vals = source1[key].strip().split()
+ newvals = []
+ for val in vals:
+ if val not in newvals:
+ newvals.append(val)
+ source1[key] = ' '.join(newvals)
+ for key in source2:
+ vals = source2[key].strip().split()
+ newvals = []
+ for val in vals:
+ if val not in newvals:
+ newvals.append(val)
+ source2[key] = ' '.join(newvals)
+
+
+def prettyprint_enable(enabled: bool = True) -> None:
+ """Easy helper to enable/disable pretty-printing for object reprs.
+
+ Can also be used as an easy way to reset to defaults.
+
+ Arguments:
+ enabled(bool): Whether or not Pretty Printing should be enabled
+ """
+ global PRETTY_PRINT
+ if enabled:
+ PRETTY_PRINT = '\n '
+ else:
+ PRETTY_PRINT = ''
+
+def url_validator(url):
+ """ Validate a url and tell if it's good or not.
+
+ Arguments:
+ url (str): The URL to validate.
+
+ Returns:
+ `True` if `url` is not malformed, otherwise `False`.
+ """
+ try:
+ # pylint: disable=no-else-return,bare-except
+ # A) We want to return false if the URL doesn't contain those parts
+ # B) We need this to not throw any exceptions, regardless what they are
+ result = urlparse(url)
+ if not result.scheme:
+ return False
+ if result.scheme == 'x-repolib-name':
+ return False
+ if result.netloc:
+ # We need at least a scheme and a netlocation/hostname or...
+ return all([result.scheme, result.netloc])
+ elif result.path:
+ # ...a scheme and a path (this allows file:/// URIs which are valid)
+ return all([result.scheme, result.path])
+ return False
+ except:
+ return False
+
+def validate_debline(valid):
+ """ Basic checks to see if a given debline is valid or not.
+
+ Arguments:
+ valid (str): The line to validate.
+
+ Returns:
+ True if the line is valid, False otherwise.
+ """
+ comment:bool = False
+ if valid.startswith('#'):
+ comment = True
+ valid = valid.replace('#', '')
+ valid = valid.strip()
+
+ if valid.startswith("deb"):
+ words = valid.split()
+ for word in words:
+ if url_validator(word):
+ return True
+
+ elif valid.startswith("ppa:"):
+ if "/" in valid:
+ return True
+
+ else:
+ if valid.endswith('.flatpakrepo'):
+ return False
+ if len(valid.split()) == 1 and not comment:
+ return url_validator(valid)
+ return False
+
+def strip_hashes(line:str) -> str:
+ """ Strips the leading #'s from the given line.
+
+ Arguments:
+ line (str): The line to strip.
+
+ Returns:
+ (str): The input line without any leading/trailing hashes or
+ leading/trailing whitespace.
+ """
+ while True:
+ line = line.strip('#')
+ line = line.strip()
+ if not line.startswith('#'):
+ break
+
+ return line
diff --git a/repolib/usr/lib/repolib/add-apt-repository b/repolib/usr/lib/repolib/add-apt-repository
new file mode 100755
index 0000000..5c2f26c
--- /dev/null
+++ b/repolib/usr/lib/repolib/add-apt-repository
@@ -0,0 +1,194 @@
+#!/usr/bin/python3
+
+"""
+Copyright (c) 2019-2020, Ian Santopietro
+All rights reserved.
+
+This file is part of RepoLib.
+
+RepoLib is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+RepoLib is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with RepoLib. If not, see .
+"""
+#pylint: disable=invalid-name
+# Pylint will complain about our module name not being snake_case, however this
+# is a command rather than a python module, and thus this is correct anyway.
+
+import argparse
+import os
+import subprocess
+
+import repolib
+
+system_codename = repolib.util.DISTRO_CODENAME
+
+system_components = [
+ 'main',
+ 'universe',
+ 'multiverse',
+ 'restricted'
+]
+
+system_suites = [
+ system_codename,
+ f'{system_codename}-updates',
+ f'{system_codename}-security',
+ f'{system_codename}-backports',
+ f'{system_codename}-proposed',
+ 'updates',
+ 'security',
+ 'backports',
+ 'proposed',
+]
+
+def get_args():
+ parser = argparse.ArgumentParser(
+ prog='add-apt-repository',
+ description=(
+ 'add-apt-repository is a script for adding apt sources.list entries.'
+ '\nThis command has been deprecated in favor of `apt-manage`. See '
+ '`apt-manage --help` for more information.'
+ )
+ )
+
+ parser.add_argument(
+ 'sourceline',
+ metavar=''
+ )
+
+ group = parser.add_mutually_exclusive_group()
+
+ group.add_argument(
+ '-m',
+ '--massive-debug',
+ dest='debug',
+ action='store_true',
+ help='Print a lot of debug information to the command line'
+ )
+
+ group.add_argument(
+ '-r',
+ '--remove',
+ action='store_true',
+ help='remove repository from sources.list.d directory'
+ )
+
+ group.add_argument(
+ '-s',
+ '--enable-source',
+ dest='source',
+ action='store_true',
+ help='Allow downloading of source packages from the repository'
+ )
+
+ parser.add_argument(
+ '-y',
+ '--yes',
+ action='store_true',
+ help='Assum yes to all queries'
+ )
+
+ parser.add_argument(
+ '-n',
+ '--no-update',
+ dest='noupdate',
+ action='store_true',
+ help='Do not update package cache after adding'
+ )
+
+ parser.add_argument(
+ '-u',
+ '--update',
+ action='store_true',
+ help='Update package cache after adding (legacy option)'
+ )
+
+ parser.add_argument(
+ '-k',
+ '--keyserver',
+ metavar='KEYSERVER',
+ help='Legacy option, unused.'
+ )
+
+ return parser
+
+parser = get_args()
+args = parser.parse_args()
+
+command = ['apt-manage']
+
+if args.debug:
+ command.append('-bb')
+
+sourceline = args.sourceline
+run = True
+remove = False
+
+if sourceline in system_components:
+ command.append('modify')
+ command.append('system')
+ if not args.remove:
+ command.append('--add-component')
+ else:
+ command.append('--remove-component')
+
+elif sourceline in system_suites:
+ command.append('modify')
+ command.append('system')
+ if not args.remove:
+ command.append('--add-suite')
+ else:
+ command.append('--remove-suite')
+
+else:
+
+ if args.source:
+ command.append('source')
+
+ elif args.remove:
+ remove = True
+ command.append('remove')
+
+ else:
+ command.append('add')
+ if not args.yes:
+ command.append('--expand')
+
+if not remove:
+ command.append(sourceline)
+else:
+ sources = repolib.get_all_sources()
+ comp_source = repolib.DebLine(sourceline)
+ for source in sources:
+ if comp_source.uris[0] in source.uris:
+ name = str(source.filename.name)
+ name = name.replace(".list", "")
+ name = name.replace(".sources", "")
+ command.append(name)
+
+run = True
+
+if os.geteuid() != 0:
+ print('Error: must run as root')
+ run = False
+
+if run:
+ subprocess.run(command)
+
+ if not args.noupdate:
+ subprocess.run(['apt', 'update'])
+
+print('NOTE: add-apt-repository is deprecated in Pop!_OS. Use this instead:')
+print_command = command.copy()
+if '--expand' in print_command:
+ print_command.remove('--expand')
+print(' '.join(print_command))
diff --git a/repolib/usr/lib/repolib/service.py b/repolib/usr/lib/repolib/service.py
new file mode 100755
index 0000000..b8d79dd
--- /dev/null
+++ b/repolib/usr/lib/repolib/service.py
@@ -0,0 +1,316 @@
+#!/usr/bin/python3
+'''
+ Copyright 2020 Ian Santopietro (ian@system76.com)
+
+ This file is part of Repolib.
+
+ Repolib is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Repolib is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Repolib. If not, see .
+'''
+#pylint: skip-file
+
+import shutil
+import subprocess
+
+import gi
+from gi.repository import GObject, GLib
+
+from pathlib import Path
+import dbus
+import dbus.service
+import dbus.mainloop.glib
+import time
+
+import repolib
+
+class RepolibException(dbus.DBusException):
+ _dbus_error_name = 'org.pop_os.repolib.RepolibException'
+
+class PermissionDeniedByPolicy(dbus.DBusException):
+ _dbus_error_name = 'org.pop_os.repolib.PermissionDeniedByPolicy'
+
+class AptException(Exception):
+ pass
+
+class Repo(dbus.service.Object):
+ def __init__(self, conn=None, object_path=None, bus_name=None):
+ dbus.service.Object.__init__(self, conn, object_path, bus_name)
+
+ # These are used by PolKit to check privileges
+ self.dbus_info = None
+ self.polkit = None
+ self.enforce_polkit = True
+
+ try:
+ self.system_repo = repolib.SystemSource()
+ except:
+ self.system_repo = None
+
+ self.source = None
+ self.sources_dir = Path('/etc/apt/sources.list.d')
+ self.keys_dir = Path('/etc/apt/trusted.gpg.d')
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='as', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def add_apt_signing_key(self, cmd, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ print(cmd)
+ key_path = str(cmd.pop(-1))
+ with open(key_path, mode='wb') as keyfile:
+ try:
+ subprocess.run(cmd, check=True, stdout=keyfile)
+ except subprocess.CalledProcessError as e:
+ raise e
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='ss', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def install_signing_key(self, src, dest, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ shutil.copy2(src, dest)
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='s', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def delete_signing_key(self, src, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ key_path = Path(src)
+ key_path.unlink()
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='s', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def delete_prefs_file(self, src, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ prefs_path = Path(src)
+ prefs_path.unlink()
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='ss', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def output_prefs_to_disk(self, path, contents, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ full_path = Path(path)
+ with open(full_path, mode='w') as output_file:
+ output_file.write(contents)
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='ss', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def output_file_to_disk(self, filename, source, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ full_path = self.sources_dir / filename
+ with open(full_path, mode='w') as output_file:
+ output_file.write(source)
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='ss', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def backup_alt_file(self, alt_file, save_file, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ alt_path = self.sources_dir / alt_file
+ save_path = self.sources_dir / save_file
+ if alt_path.exists():
+ alt_path.rename(save_path)
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='s', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def delete_source_file(self, filename, sender=None, conn=None):
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ source_file = self.sources_dir / filename
+ source_file.unlink()
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='', out_signature='',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def exit(self, sender=None, conn=None):
+ mainloop.quit()
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='b', out_signature='b',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def set_system_source_code_enabled(self, enabled, sender=None, conn=None):
+ """ Enable or disable source code in the system source.
+
+ Arguments:
+ enabled (bool): The new state to set, True = Enabled.
+ """
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ if self.system_repo:
+ self.system_repo.load_from_file()
+ new_types = [repolib.util.AptSourceType.BINARY]
+ if enabled:
+ new_types.append(repolib.util.AptSourceType.SOURCE)
+ self.system_repo.types = new_types
+ self.system_repo.save_to_disk()
+ return enabled
+ return False
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='sb', out_signature='b',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def set_system_comp_enabled(self, comp, enable, sender=None, conn=None):
+ """ Enable or disable a component in the system source.
+
+ Arguments:
+ comp (str): the component to set
+ enable (bool): The new state to set, True = Enabled.
+ """
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ if self.system_repo:
+ self.system_repo.load_from_file()
+ self.system_repo.set_component_enabled(component=comp, enabled=enable)
+ self.system_repo.save_to_disk()
+ return True
+ return False
+
+ @dbus.service.method(
+ "org.pop_os.repolib.Interface",
+ in_signature='sb', out_signature='b',
+ sender_keyword='sender', connection_keyword='conn'
+ )
+ def set_system_suite_enabled(self, suite, enable, sender=None, conn=None):
+ """ Enable or disable a suite in the system source.
+
+ Arguments:
+ suite (str): the suite to set
+ enable (bool): The new state to set, True = Enabled.
+ """
+ self._check_polkit_privilege(
+ sender, conn, 'org.pop_os.repolib.modifysources'
+ )
+ if self.system_repo:
+ self.system_repo.load_from_file()
+ self.system_repo.set_suite_enabled(suite=suite, enabled=enable)
+ self.system_repo.save_to_disk()
+ return True
+ return False
+
+ @classmethod
+ def _log_in_file(klass, filename, string):
+ date = time.asctime(time.localtime())
+ ff = open(filename, "a")
+ ff.write("%s : %s\n" %(date,str(string)))
+ ff.close()
+
+ @classmethod
+ def _strip_source_line(self, source):
+ source = source.replace("#", "# ")
+ source = source.replace("[", "")
+ source = source.replace("]", "")
+ source = source.replace("'", "")
+ source = source.replace(" ", " ")
+ return source
+
+ def _check_polkit_privilege(self, sender, conn, privilege):
+ # from jockey
+ '''Verify that sender has a given PolicyKit privilege.
+ sender is the sender's (private) D-BUS name, such as ":1:42"
+ (sender_keyword in @dbus.service.methods). conn is
+ the dbus.Connection object (connection_keyword in
+ @dbus.service.methods). privilege is the PolicyKit privilege string.
+ This method returns if the caller is privileged, and otherwise throws a
+ PermissionDeniedByPolicy exception.
+ '''
+ if sender is None and conn is None:
+ # called locally, not through D-BUS
+ return
+ if not self.enforce_polkit:
+ # that happens for testing purposes when running on the session
+ # bus, and it does not make sense to restrict operations here
+ return
+
+ # get peer PID
+ if self.dbus_info is None:
+ self.dbus_info = dbus.Interface(conn.get_object('org.freedesktop.DBus',
+ '/org/freedesktop/DBus/Bus', False), 'org.freedesktop.DBus')
+ pid = self.dbus_info.GetConnectionUnixProcessID(sender)
+
+ # query PolicyKit
+ if self.polkit is None:
+ self.polkit = dbus.Interface(dbus.SystemBus().get_object(
+ 'org.freedesktop.PolicyKit1',
+ '/org/freedesktop/PolicyKit1/Authority', False),
+ 'org.freedesktop.PolicyKit1.Authority')
+ try:
+ # we don't need is_challenge return here, since we call with AllowUserInteraction
+ (is_auth, _, details) = self.polkit.CheckAuthorization(
+ ('unix-process', {'pid': dbus.UInt32(pid, variant_level=1),
+ 'start-time': dbus.UInt64(0, variant_level=1)}),
+ privilege, {'': ''}, dbus.UInt32(1), '', timeout=600)
+ except dbus.DBusException as e:
+ if e._dbus_error_name == 'org.freedesktop.DBus.Error.ServiceUnknown':
+ # polkitd timed out, connect again
+ self.polkit = None
+ return self._check_polkit_privilege(sender, conn, privilege)
+ else:
+ raise
+
+ if not is_auth:
+ Repo._log_in_file('/tmp/repolib.log','_check_polkit_privilege: sender %s on connection %s pid %i is not authorized for %s: %s' %
+ (sender, conn, pid, privilege, str(details)))
+ raise PermissionDeniedByPolicy(privilege)
+
+if __name__ == '__main__':
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+ bus = dbus.SystemBus()
+ name = dbus.service.BusName("org.pop_os.repolib", bus)
+ object = Repo(bus, '/Repo')
+
+ mainloop = GLib.MainLoop()
+ mainloop.run()
\ No newline at end of file
diff --git a/repolib/usr/share/bash-completion/completions/apt-manage b/repolib/usr/share/bash-completion/completions/apt-manage
new file mode 100755
index 0000000..f881bc4
--- /dev/null
+++ b/repolib/usr/share/bash-completion/completions/apt-manage
@@ -0,0 +1,42 @@
+# Debian apt-manage completion
+
+_apt_manage()
+{
+ local cur prev words cword package
+ _init_completion -n ':=' || return
+
+ local special i
+ i=0
+ for (( i=0; i < ${#words[@]}-1; i++ )); do
+ if [[ ${words[i]} == @(add|list|modify|remove|key) ]]; then
+ special=${words[i]}
+ fi
+ done
+
+ if [[ -n $special ]]; then
+ case $special in
+ list|modify|remove|key)
+ COMPREPLY=( $( compgen -W '$(apt-manage list -n)' -- "$cur" ) )
+ return
+ ;;
+ *)
+ ;;
+ esac
+ fi
+
+
+ if [[ "$cur" == -* ]]; then
+ return
+ # COMPREPLY=( $(compgen -W '
+ # --help --disable --source-code --expand
+ # --verbose --legacy --no-names
+ # --enable --disable --name --add-suite --remove-suite
+ # --add-component --remove-component --add-uri --remove-uri
+ # ' -- "$cur") )
+ else
+ COMPREPLY=( $(compgen -W 'add list modify remove key' \
+ -- "$cur") )
+ fi
+
+} &&
+complete -F _apt_manage apt-manage
diff --git a/repolib/usr/share/dbus-1/system-services/org.pop_os.repolib.service b/repolib/usr/share/dbus-1/system-services/org.pop_os.repolib.service
new file mode 100644
index 0000000..2f354e3
--- /dev/null
+++ b/repolib/usr/share/dbus-1/system-services/org.pop_os.repolib.service
@@ -0,0 +1,4 @@
+[D-BUS Service]
+Name=org.pop_os.repolib
+Exec=/usr/bin/python3 /usr/lib/repolib/service.py
+User=root
diff --git a/repolib/usr/share/polkit-1/actions/org.pop_os.repolib.policy b/repolib/usr/share/polkit-1/actions/org.pop_os.repolib.policy
new file mode 100644
index 0000000..7a6caef
--- /dev/null
+++ b/repolib/usr/share/polkit-1/actions/org.pop_os.repolib.policy
@@ -0,0 +1,20 @@
+
+
+
+
+ Repoman
+ https://github.com/pop-os/repolib
+ x-system-software-sources
+
+
+ Modifies a Debian Repository in the system software sources.
+ Authentication is required to change software sources.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
\ No newline at end of file
diff --git a/repolib/usr/share/zsh/vendor-completions/_apt-manage b/repolib/usr/share/zsh/vendor-completions/_apt-manage
new file mode 100644
index 0000000..ffe1fb1
--- /dev/null
+++ b/repolib/usr/share/zsh/vendor-completions/_apt-manage
@@ -0,0 +1,71 @@
+#compdef apt-manage
+typeset -A opt_args
+
+typeset -A opt_args
+
+_arguments -C \
+ '1:cmd:->cmds' \
+ '2:sources:->source_lists' \
+ '*:: :->args' \
+&& ret=0
+
+case "$state" in
+ (cmds)
+ local commands; commands=(
+ 'add'
+ 'list'
+ 'modify'
+ 'remove'
+ 'key'
+ )
+ _describe -t commands 'command' commands && ret=0
+ ;;
+ (source_lists)
+ local sources
+ sources=( $(apt-manage list -n))
+ _describe -t sources 'source' sources && ret=0
+ ;;
+ (args)
+ local arguments
+ arguments=(
+ # Main command
+ '--help'
+ # Add subcommand
+ '--disable'
+ '--source-code'
+ '--terse'
+ '--name'
+ '--identifier'
+ '--format'
+ '--skip-keys'
+ # Key Ssbcommand
+ '--path'
+ '--url'
+ '--ascii'
+ '--fingerprint'
+ '--keyserver'
+ '--info'
+ '--remove'
+ # List subcommand
+ '--legacy'
+ '--verbose'
+ '--all'
+ '--file-names'
+ # Modify subcommand
+ '--enable'
+ '--source-enable'
+ '--source-disable'
+ '--add-uri'
+ '--add-suite'
+ '--add-component'
+ '--remove-uri'
+ '--remove-suite'
+ '--remove-component'
+ # Remove subcommand
+ '--assume-yes'
+ )
+ _describe -t arguments 'argument' arguments && ret=0
+ ;;
+esac
+
+return 1
\ No newline at end of file