Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion PortMaster/pugwash
Original file line number Diff line number Diff line change
Expand Up @@ -953,17 +953,28 @@ class PortMasterGUI(pySDL2gui.GUI, harbourmaster.Callback):
self.set_data("port_info.instructions", "")
self.set_data("port_info.genres", "")
self.set_data("port_info.porter", "")
self.set_data("port_info.source", "")
self.set_data("port_info.ready_to_run", "")
self.set_data("port_info.runtime", "")
self.set_data("port_info.download_size", "")
self.set_data("port_info.install_size", "")
# self.set_data("port_info.image", "no-image")
return

# 'Unknown', 'file' and 'url' mean manual install
port_source = port_info.get('status', {}).get('source', None)
if port_source in (None, "", "Unknown", "file", "url"):
port_source = port_info.get('source', {}).get('repo', None) or ""

description = port_info['attr']['desc']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why inject the source into the description? Wouldn't it be better to keep these two semantic things distinct and have the source displayed in the layout side of things instead of polluting description?
Ok, I read the submission wall of text, but why would I care about the source of a port if I was running an old version of portmaster that doesn't pull from any different sources? Also I don't think we want to broadcast MV sources.

@JeodC JeodC Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback you're referring to targets old theme zips, not old GUI versions. It allows old theme zips to display the new variable. As for description injection, fine to remove if not wanted but the port info list is a small font that can be overlapped by a screenshot. I was going for visibility.

With MV, it's due to the presence of two files. The source comes from the name key. I can add a way to merge the two official sources into a single display.

if port_source != "":
description += "\n\n" + _("Source: {port_source}").format(port_source=port_source)

self.set_data("port_info.name", port_name)
self.set_data("port_info.image", str(self.get_port_image(port_name)))
self.set_data("port_info.title", port_info['attr']['title'])
self.set_data("port_info.description", port_info['attr']['desc'])
self.set_data("port_info.description", description)
self.set_data("port_info.source", port_source)
self.set_data("port_info.instructions", port_info['attr']['inst'])
self.set_data("port_info.genres", ', '.join(port_info['attr']['genres']))
self.set_data("port_info.porter", harbourmaster.oc_join(port_info['attr']['porter']))
Expand Down
6 changes: 3 additions & 3 deletions PortMaster/pylibs/default_theme/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@
"font-size[16:9]": 11,
"font-color": "general_font",
"text-wrap": true,
"text": "Genres: {port_info.genres}\n{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}"
"text": "Genres: {port_info.genres}\n{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\nSource: {port_info.source}{endif}"
},
"port_desc": {
"parent": "port_info_text_area",
Expand Down Expand Up @@ -415,7 +415,7 @@
},
"port_info_instruction": {
"area": [ 0.6, 0.1, 0.95, 0.65 ],
"text": "Free Space: {system.free_space}\n\nPorter: {port_info.porter}\nGenres: {port_info.genres}\nDownload Size: {port_info.download_size}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}\n\nInstruction: {port_info.instructions}",
"text": "Free Space: {system.free_space}\n\nPorter: {port_info.porter}\nGenres: {port_info.genres}\nDownload Size: {port_info.download_size}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\nSource: {port_info.source}{endif}\n\nInstruction: {port_info.instructions}",
"fill": "panels",
"thickness": 6,
"border": 8,
Expand Down Expand Up @@ -843,7 +843,7 @@
"font-size[16:9]": 11,
"font-color": "general_font",
"text-wrap": true,
"text": "Genres: {port_info.genres}\n{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}"
"text": "Genres: {port_info.genres}\n{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\nRuntime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\nSource: {port_info.source}{endif}"
},

"featured_port_description": {
Expand Down
37 changes: 37 additions & 0 deletions PortMaster/pylibs/harbourmaster/harbour.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,15 @@ def load_sources(self):
self.callback.message(" - {}".format(_("Loading Sources.")))

check_keys = {'version': None, 'prefix': None, 'api': HM_SOURCE_APIS, 'name': None, 'last_checked': None, 'data': None}

reserved_prefixes = {}
reserved_names = {}
for default_file, default_text in HM_SOURCE_DEFAULTS.items():
default_data = json_safe_loads(default_text)
if default_data is not None:
reserved_prefixes[default_data['prefix']] = default_file
reserved_names[default_data['name']] = default_file

for source_file in source_files:
with source_file.open() as fh:
source_data = json_safe_load(fh)
Expand Down Expand Up @@ -536,10 +545,35 @@ def load_sources(self):
source_data['last_checked'] = None
source_data['data'] = {}

# A reserved prefix only loads from its bundled file, even if the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of extra code to protect against a security issue that's not really relevant. "Imposter" source files are fine if somebody wants to do that on their handheld.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're looking at user control, and I agree, however my approach has more to do with injection. Not that I would do this, but take Pharos for example. Pharos can already download my gmtoolkit.aarch64 binary and put it in $controlfolder for a user. It is entirely possible for a bad actor to sideload a third-party repo file without the user's consent.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??? we run ports at root, a bad actor can do whatever tf they want. Unless you can demonstrate a remote vuln without device or access to an installed port (and don't give me DNS poisoning like I mean something real) this is a dead end and I would vote against adding cruft for a non-issue.

This feels to me like an LLM told you that this is a security vulnerability because the LLM doesn't understand the zero-security model we use for handheld devices where we run unaudited chinese firmwares and assume that devices are already rootkitted so our model is to just keep any sensitive info off-device (and frankly suggest partitioning your home network too).

@JeodC JeodC Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is me recognizing JohnnyOnFlame's concerns, finding that when I use portmaster gui's infrastructure as advertised that my port is indistinguishable from a portmaster official port, and promptly fixing it because if I can do it, then so can anyone else.

Ok not to add. Just pointing it out and attempting to contribute a solution.

# imposter file sorts first.
if reserved_prefixes.get(source_data['prefix'], source_file.name) != source_file.name:
logger.error(f"Reserved prefix {source_data['prefix']!r} in {source_file}, skipping.")
continue

# First file wins a prefix; a later source can't replace it.
if source_data['prefix'] in self.sources:
logger.error(f"Duplicate source prefix {source_data['prefix']!r} in {source_file}, skipping.")
continue

source = HM_SOURCE_APIS[source_data['api']](self, source_file, source_data)

self.sources[source_data['prefix']] = source

name_counts = {}
for source in self.sources.values():
name_counts[source.name] = name_counts.get(source.name, 0) + 1

for source in self.sources.values():
file_name = source._file_name.name
if source.name in reserved_names:
if file_name == reserved_names[source.name]:
continue
elif name_counts[source.name] < 2:
continue

source.display_name = f"{source.name} ({file_name.rsplit('.source.json', 1)[0]})"


def _get_pm_signature(self, file_name):
"""
Expand Down Expand Up @@ -1579,6 +1613,9 @@ def port_info(self, port_name, installed=False):
else:
result = port_info_load(source.port_info(port_name))

if result is not None and 'repo' not in result.get('source', {}):
result.setdefault('source', {})['repo'] = source.display_name

if result is None:
self.__PORT_INFO_CACHE[port_key] = result
return None
Expand Down
10 changes: 6 additions & 4 deletions PortMaster/pylibs/harbourmaster/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def __init__(self, hm, file_name, config):
self._file_name = file_name
self._config = config
self._prefix = config['prefix']
# Overridden by load_sources when the name collides with another source.
self.display_name = config['name']
self._did_update = False
self._wants_update = None
self._images_dir = self.hm.cfg_dir / f"images_{self._prefix}"
Expand Down Expand Up @@ -429,7 +431,7 @@ def download(self, port_name, temp_dir=None, md5_result=None):

zip_info['name'] = port_name
zip_info['status'] = {
'source': self._config['name'],
'source': self.display_name,
'md5': md5_result[0],
'status': 'downloaded',
}
Expand Down Expand Up @@ -559,7 +561,7 @@ def download(self, port_name, temp_dir=None, md5_result=None):

zip_info['name'] = port_name
zip_info['status'] = {
'source': self._config['name'],
'source': self.display_name,
'md5': md5_result[0],
'status': 'downloaded',
}
Expand Down Expand Up @@ -684,7 +686,7 @@ def download(self, port_name, temp_dir=None, md5_result=None):

zip_info['name'] = name_cleaner(port_name)
zip_info['status'] = {
'source': self._config['name'],
'source': self.display_name,
'md5': md5_result[0],
'status': 'downloaded',
}
Expand Down Expand Up @@ -1083,7 +1085,7 @@ def download(self, port_name, temp_dir=None, md5_result=None):

zip_info['name'] = port_name
zip_info['status'] = {
'source': self._config['name'],
'source': self.display_name,
'md5': md5_result[0],
'status': 'downloaded',
}
Expand Down
85 changes: 85 additions & 0 deletions PortMaster/pylibs/locales/de_DE/LC_MESSAGES/themes.po
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,88 @@ msgstr "Genres: {port_info.genres}\n"
"{if:port_info.install_size}Installierte Größe: {port_info.install_size}{else}Downloadgröße: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}"

#: default_theme/theme.json:347, default_theme/theme.json:846
msgid ""
"Genres: {port_info.genres}\n"
"{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""
"Genres: {port_info.genres}\n"
"{if:port_info.install_size}Installierte Größe: {port_info.install_size}{else}Downloadgröße: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: default_theme/theme.json:418
msgid ""
"Free Space: {system.free_space}\n"
"\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"\n"
"Instruction: {port_info.instructions}"
msgstr ""
"Freier Speicher: {system.free_space}\n"
"\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Downloadgröße: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"\n"
"Anleitung: {port_info.instructions}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: Zelda.theme.zip/theme.json
msgid ""
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""
"Genres: {port_info.genres} \n"
"Download-Größe: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: Doom.theme.zip/theme.json, Fallout.theme.zip/theme.json, Stardew.theme.zip/theme.json, ff_VII.theme.zip/theme.json
msgid "Genres: {port_info.genres} Download Size: {port_info.download_size}{if:port_info.runtime} Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source} Source: {port_info.source}{endif}"
msgstr "Genres: {port_info.genres} Download-Größe: {port_info.download_size}{if:port_info.runtime} Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source} Source: {port_info.source}{endif}"

#: basic_theme.theme.zip/theme.json
msgid ""
"Title: {port_info.title}\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"Description: {port_info.description}"
msgstr ""
"Titel: {port_info.title}\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Downloadgröße: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"Beschreibung: {port_info.description}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: hiviz.theme.zip/theme.json
msgid ""
"• Genres: {port_info.genres}\n"
"• Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"• Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"• Source: {port_info.source}{endif}\n"
"• Description: {port_info.description}\n"
"• Instructions:\n"
"{port_info.instructions}"
msgstr ""
"• Genres: {port_info.genres}\n"
"• Downloadgröße: {port_info.download_size}{if:port_info.runtime}\n"
"• Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"• Beschreibung: {port_info.description}\n"
"• Anleitungen:\n"
"{port_info.instructions}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
54 changes: 54 additions & 0 deletions PortMaster/pylibs/locales/en_US/LC_MESSAGES/themes.po
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,57 @@ msgstr ""
msgid "Time: {system.time_24hr} Battery: {system.battery_level}"
msgstr ""

#: default_theme/theme.json:347, default_theme/theme.json:846
msgid ""
"Genres: {port_info.genres}\n"
"{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""

#: default_theme/theme.json:418
msgid ""
"Free Space: {system.free_space}\n"
"\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"\n"
"Instruction: {port_info.instructions}"
msgstr ""

#: Zelda.theme.zip/theme.json
msgid ""
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""

#: Doom.theme.zip/theme.json, Fallout.theme.zip/theme.json, Stardew.theme.zip/theme.json, ff_VII.theme.zip/theme.json
msgid "Genres: {port_info.genres} Download Size: {port_info.download_size}{if:port_info.runtime} Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source} Source: {port_info.source}{endif}"
msgstr ""

#: basic_theme.theme.zip/theme.json
msgid ""
"Title: {port_info.title}\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"Description: {port_info.description}"
msgstr ""

#: hiviz.theme.zip/theme.json
msgid ""
"• Genres: {port_info.genres}\n"
"• Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"• Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"• Source: {port_info.source}{endif}\n"
"• Description: {port_info.description}\n"
"• Instructions:\n"
"{port_info.instructions}"
msgstr ""
85 changes: 85 additions & 0 deletions PortMaster/pylibs/locales/es_ES/LC_MESSAGES/themes.po
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,88 @@ msgstr "Géneros: {port_info.genres}\n"
"{if:port_info.install_size}Tamaño de Instalación: {port_info.install_size}{else}Tamaño de Descarga: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}"

#: default_theme/theme.json:347, default_theme/theme.json:846
msgid ""
"Genres: {port_info.genres}\n"
"{if:port_info.install_size}Installed Size: {port_info.install_size}{else}Download Size: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""
"Géneros: {port_info.genres}\n"
"{if:port_info.install_size}Tamaño de Instalación: {port_info.install_size}{else}Tamaño de Descarga: {port_info.download_size}{endif}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: default_theme/theme.json:418
msgid ""
"Free Space: {system.free_space}\n"
"\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"\n"
"Instruction: {port_info.instructions}"
msgstr ""
"Espacio Libre: {system.free_space}\n"
"\n"
"Porteador: {port_info.porter}\n"
"Generos: {port_info.genres}\n"
"Tamaño de Descarga: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"\n"
"Instrucciones: {port_info.instructions}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: Zelda.theme.zip/theme.json
msgid ""
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
msgstr ""
"Géneros: {port_info.genres}\n"
"Tamaño de Descarga: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: Doom.theme.zip/theme.json, Fallout.theme.zip/theme.json, Stardew.theme.zip/theme.json, ff_VII.theme.zip/theme.json
msgid "Genres: {port_info.genres} Download Size: {port_info.download_size}{if:port_info.runtime} Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source} Source: {port_info.source}{endif}"
msgstr "Géneros: {port_info.genres} Tamaño de Descarga: {port_info.download_size}{if:port_info.runtime} Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source} Source: {port_info.source}{endif}"

#: basic_theme.theme.zip/theme.json
msgid ""
"Title: {port_info.title}\n"
"Porter: {port_info.porter}\n"
"Genres: {port_info.genres}\n"
"Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"Source: {port_info.source}{endif}\n"
"Description: {port_info.description}"
msgstr ""
"Título: {port_info.title}\n"
"Porteador: {port_info.porter}\n"
"Géneros: {port_info.genres}\n"
"Tamaño de Descarga: {port_info.download_size}{if:port_info.runtime}\n"
"Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"Descripción: {port_info.description}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"

#: hiviz.theme.zip/theme.json
msgid ""
"• Genres: {port_info.genres}\n"
"• Download Size: {port_info.download_size}{if:port_info.runtime}\n"
"• Runtime: {port_info.runtime} ({port_info.runtime_status}){endif}{if:port_info.source}\n"
"• Source: {port_info.source}{endif}\n"
"• Description: {port_info.description}\n"
"• Instructions:\n"
"{port_info.instructions}"
msgstr ""
"• Géneros: {port_info.genres}\n"
"• Tamaño descargado: {port_info.download_size}{if:port_info.runtime}\n"
"• Tiempo de ejecución: {port_info.runtime} ({port_info.runtime_status}){endif}\n"
"• Descripción: {port_info.description}\n"
"• Instrucciones:\n"
"{port_info.instructions}{if:port_info.source}\n"
"Source: {port_info.source}{endif}"
Loading