import logging
import os
import re
import rpm # type: ignore
from Registry import Registry # type: ignore
from . import log, subdirs
__all__ = [
"list_applications_apk",
"list_applications_dpkg",
"list_applications_pacman",
"list_applications_portage",
"list_applications_rpm",
"list_applications_windows"
]
L = logging.getLogger(__name__)
@log
def list_applications_apk(path):
"""Find all packages installed on the linux distribution Alpine Linux.
See also:
https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper
Args:
path (str): Path to the mounted filesystem.
Returns:
List of packages. For example:
[{'name': 'musl', 'version': '1.2.3-r0'}, ...]
"""
apk_db = None
location = "lib/apk/db/installed"
db = os.path.join(path, location)
if os.path.exists(db):
apk_db = db
if apk_db is None:
for dir in subdirs(path):
new_path = os.path.join(path, dir)
db = os.path.join(new_path, location)
if os.path.exists(db):
apk_db = db
break
if apk_db is None:
L.debug("apk database not found")
return []
pkgs = []
with open(apk_db) as f:
name = version = ""
for line in f:
line = line.strip()
if not line:
if name and version:
pkgs.append({
"name": name,
"version": version
})
name = version = ""
elif line.startswith("P:"):
name = line[2:]
elif line.startswith("V:"):
version = line[2:]
return pkgs
@log
def list_applications_dpkg(path):
"""Find all packages installed on a debian-based linux distribution.
See also:
https://man7.org/linux/man-pages/man1/dpkg.1.html
Args:
path (str): Path to the mounted filesystem.
Returns:
List of packages. For example:
[{'name': 'adduser', 'version': '3.118'}, ...]
"""
dpkg_db = None
locations = [
"var/lib/dpkg/status",
"lib/dpkg/status" # separated /var partition
]
for location in locations:
db = os.path.join(path, location)
if os.path.exists(db):
dpkg_db = db
break
# Debian uses subvol=@rootfs for root filesystem for btrfs.
# Therefore, under Debian 11.* looks like this: /@rootfs/var/lib/dpkg
if dpkg_db is None:
for dir in subdirs(path):
new_path = os.path.join(path, dir)
for location in locations:
db = os.path.join(new_path, location)
if os.path.exists(db):
dpkg_db = db
break
else:
continue
break
if dpkg_db is None:
L.debug("dpkg database not found")
return []
pkgs = []
with open(dpkg_db) as f:
name = version = ""
installed = False
for line in f:
line = line.strip()
if not line:
if name and version and installed:
pkgs.append({
"name": name,
"version": version
})
name = version = ""
installed = False
elif line.startswith("Package:"):
name = line[9:]
elif line.startswith("Status:"):
installed = "installed" in line[8:].split()
elif line.startswith("Version:"):
version = line[9:]
return pkgs
@log
def list_applications_pacman(path):
"""Find all packages installed on a arch-based linux distribution.
See also:
https://wiki.archlinux.org/title/pacman
Args:
path (str): Path to the mounted filesystem.
Returns:
List of packages. For example:
[{'name': 'python', 'version': '3.10.6-1'}, ...]
"""
pacman_db = None
locations = [
"var/lib/pacman/local",
"lib/pacman/local" # separated /var partition
]
for location in locations:
db = os.path.join(path, location)
if os.path.exists(db):
pacman_db = db
break
# Just in case the system is using btrfs.
if pacman_db is None:
for dir in subdirs(path):
new_path = os.path.join(path, dir)
for location in locations:
db = os.path.join(new_path, location)
if os.path.exists(db):
pacman_db = db
break
else:
continue
break
if pacman_db is None:
L.debug("pacman database not found")
return []
pkgs = []
for dir in subdirs(pacman_db):
desc = os.path.join(pacman_db, dir, "desc")
kv = {}
with open(desc) as f:
sections = re.split(r"\n(?=%[A-Z]+%)", f.read())
for section in sections:
section = section.strip()
if lines := section.split("\n"):
k = lines[0].strip("%")
n = len(lines)
v = "" if n < 2 else lines[1:] if n > 2 else lines[1]
kv[k] = v
if (name := kv.get("NAME")) and (version := kv.get("VERSION")):
pkgs.append({
"name": name,
"version": version
})
return pkgs
@log
def list_applications_portage(path):
"""Find all packages installed on the linux distribution Gentoo Linux.
See also:
https://wiki.gentoo.org/wiki/Portage
Args:
path (str): Path to the mounted filesystem.
Returns:
List of packages. For example:
[{'name': 'sys-devel/bison', 'version': '3.8.2'}, ...]
"""
portage_db = None
locations = [
"var/db/pkg",
"db/pkg" # separated /var partition
]
for location in locations:
db = os.path.join(path, location)
if os.path.exists(db):
portage_db = db
break
# btrfs?
if portage_db is None:
for dir in subdirs(path):
new_path = os.path.join(path, dir)
for location in locations:
db = os.path.join(new_path, location)
if os.path.exists(db):
portage_db = db
break
else:
continue
break
if portage_db is None:
L.debug("portage database not found")
return []
pkgs = []
for cat in subdirs(portage_db):
for pkg in subdirs(os.path.join(portage_db, cat)):
# https://projects.gentoo.org/pms/8/pms.html#x1-150003
if m := re.match(r"^(.+)-(\d.*)$", pkg):
name, version = m.groups()
pkgs.append({
"name": "/".join([cat, name]),
"version": version
})
return pkgs
@log
def list_applications_rpm(path):
"""Find all packages installed on a rpm-based linux distribution.
Args:
path (str): Path to the mounted filesystem.
Returns:
List of packages. For example:
[{'name': 'libgcc', 'version': '12.0.1'}, ...]
"""
rpm_db = None
for root, dirs, _ in os.walk(path):
if "usr" in dirs:
db = os.path.join(root, "usr/share/rpm")
if os.path.exists(db):
rpm_db = db
break
# https://fedoraproject.org/wiki/Changes/RelocateRPMToUsr
db = os.path.join(root, "usr/lib/sysimage/rpm")
if os.path.exists(db):
rpm_db = db
break
if "var" in dirs:
db = os.path.join(root, "var/lib/rpm")
if os.path.exists(db):
rpm_db = db
break
if rpm_db is None:
L.debug("RPM database not found")
return []
rpm.setVerbosity(rpm.RPMLOG_CRIT)
rpm.addMacro("_dbpath", rpm_db)
ts = rpm.TransactionSet()
try:
dbMatch = ts.dbMatch()
except Exception as e:
L.error("failed to open RPM database: %r", e)
return []
pkgs = []
for h in dbMatch:
pkgs.append({
"name": h["name"],
"version": h["version"]
})
rpm.delMacro("_dbpath")
return pkgs
@log
def list_applications_windows(path):
"""Find all applications installed on a windows distribution.
Args:
path (str): Path to the mounted filesystem.
Returns:
List of applications. For example:
[{'name': 'Mozilla Firefox 43.0.1 (x86 de)', 'version': '43.0.1'}, ...]
"""
software = None
locations = [
"WINDOWS/system32/config/software", # xp
"Windows/System32/config/SOFTWARE", # others
]
for location in locations:
software_path = os.path.join(path, location)
if os.path.isfile(software_path):
software = software_path
break
if not software:
L.debug("software hive not found in %s", path)
return []
try:
registry = Registry.Registry(software)
except Exception as e:
L.error("failed to open registry file %s: %r", software, e)
return []
apps = []
# native applications
hive_path = "Microsoft\\Windows\\CurrentVersion\\Uninstall"
try:
key = registry.open(hive_path)
except Exception as e:
L.error("%s not found in %s: %r", hive_path, software, e)
return apps
if apps_native := _list_applications_windows_from_key(key):
apps.extend(apps_native)
# 32-bit applications running on WOW64 emulator
# see also: http://support.microsoft.com/kb/896459
hive_path = "Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
try:
key = registry.open(hive_path)
except Exception as e:
L.error("%s not found in %s: %r", hive_path, software, e)
return apps
if apps_emulator := _list_applications_windows_from_key(key):
apps.extend(apps_emulator)
return apps
@log
def _list_applications_windows_from_key(key):
"""Parse applications from windows registry key.
See also:
https://docs.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key
Args:
key (Registry.Key): Registry key.
Returns:
List of applications.
"""
apps = []
for k in key.subkeys():
# name = k.name()
# name does not say much, so take the display name
name = version = ""
for v in k.values():
if v.name() == "DisplayName":
name = v.value()
if v.name() == "DisplayVersion":
version = v.value()
# ignore applications with no display name
if name and version:
apps.append({
"name": name,
"version": version
})
return apps