python-pid/archive/repolib/util.py

456 lines
12 KiB
Python
Raw Normal View History

2023-02-23 21:05:14 +01:00
#!/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 <https://www.gnu.org/licenses/>.
"""
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