standardize config on ruamel.yaml with a YAML 1.2 config

This is a key piece of the ongoing `PUBLISH` _config.yml_ migration. There was uneven implementation of which YAML parser to use, and that could lead to bugs where one parser might read a value one way, and a different parser will read the value a different way. I wanted to be sure that YAML 1.2 would always work.

This makes all code that handles config files use the same `ruamel.yaml` parsers.  This only touches other usages of YAML parsers when there is overlap.  This does not port all of _fdroidserver_ to `ruamel.yaml` and YAML 1.2.  The metadata files should already be YAML 1.2 anyway.

# Conflicts:
#	fdroidserver/lint.py
This commit is contained in:
Hans-Christoph Steiner 2025-03-07 14:13:21 +01:00
parent 53b62415d3
commit 2f47938dbf
15 changed files with 116 additions and 48 deletions

40
fdroidserver/_yaml.py Normal file
View file

@ -0,0 +1,40 @@
# Copyright (C) 2025, Hans-Christoph Steiner <hans@eds.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Standard YAML parsing and dumping.
YAML 1.2 is the preferred format for all data files. When loading
F-Droid formats like config.yml and <Application ID>.yml, YAML 1.2 is
forced, and older YAML constructs should be considered an error.
It is OK to load and dump files in other YAML versions if they are
externally defined formats, like FUNDING.yml. In those cases, these
common instances might not be appropriate to use.
There is a separate instance for dumping based on the "round trip" aka
"rt" mode. The "rt" mode maintains order while the "safe" mode sorts
the output. Also, yaml.version is not forced in the dumper because that
makes it write out a "%YAML 1.2" header. F-Droid's formats are
explicitly defined as YAML 1.2 and meant to be human-editable. So that
header gets in the way.
"""
import ruamel.yaml
yaml = ruamel.yaml.YAML(typ='safe')
yaml.version = (1, 2)
yaml_dumper = ruamel.yaml.YAML(typ='rt')

View file

@ -39,6 +39,7 @@ import sys
import re
import ast
import gzip
import ruamel.yaml
import shutil
import stat
import subprocess
@ -48,7 +49,6 @@ import logging
import hashlib
import socket
import base64
import yaml
import zipfile
import tempfile
import json
@ -67,6 +67,7 @@ from zipfile import ZipFile
import fdroidserver.metadata
from fdroidserver import _
from fdroidserver._yaml import yaml, yaml_dumper
from fdroidserver.exception import FDroidException, VCSException, NoSubmodulesException, \
BuildException, VerificationException, MetaDataException
from .asynchronousfilereader import AsynchronousFileReader
@ -549,7 +550,7 @@ def read_config():
if os.path.exists(CONFIG_FILE):
logging.debug(_("Reading '{config_file}'").format(config_file=CONFIG_FILE))
with open(CONFIG_FILE, encoding='utf-8') as fp:
config = yaml.safe_load(fp)
config = yaml.load(fp)
if not config:
config = {}
config_type_check(CONFIG_FILE, config)
@ -706,7 +707,7 @@ def load_localized_config(name, repodir):
if len(f.parts) == 2:
locale = DEFAULT_LOCALE
with open(f, encoding="utf-8") as fp:
elem = yaml.safe_load(fp)
elem = yaml.load(fp)
if not isinstance(elem, dict):
msg = _('{path} is not "key: value" dict, but a {datatype}!')
raise TypeError(msg.format(path=f, datatype=type(elem).__name__))
@ -4229,7 +4230,9 @@ def write_to_config(thisconfig, key, value=None):
lines[-1] += '\n'
pattern = re.compile(r'^[\s#]*' + key + r':.*\n')
repl = yaml.dump({key: value})
with ruamel.yaml.compat.StringIO() as fp:
yaml_dumper.dump({key: value}, fp)
repl = fp.getvalue()
# If we replaced this line once, we make sure won't be a
# second instance of this line for this key in the document.

View file

@ -26,7 +26,6 @@ import json
import logging
import os
import re
import ruamel.yaml
import shutil
import sys
import tempfile
@ -45,6 +44,7 @@ from . import metadata
from . import net
from . import signindex
from fdroidserver.common import ANTIFEATURES_CONFIG_NAME, CATEGORIES_CONFIG_NAME, CONFIG_CONFIG_NAME, MIRRORS_CONFIG_NAME, RELEASECHANNELS_CONFIG_NAME, DEFAULT_LOCALE, FDroidPopen, FDroidPopenBytes, load_stats_fdroid_signing_key_fingerprints
from fdroidserver._yaml import yaml
from fdroidserver.exception import FDroidException, VerificationException
@ -1445,7 +1445,7 @@ def add_mirrors_to_repodict(repo_section, repodict):
)
)
with mirrors_yml.open() as fp:
mirrors_config = ruamel.yaml.YAML(typ='safe').load(fp)
mirrors_config = yaml.load(fp)
if not isinstance(mirrors_config, list):
msg = _('{path} is not list, but a {datatype}!')
raise TypeError(

View file

@ -24,9 +24,8 @@ import urllib.parse
from argparse import ArgumentParser
from pathlib import Path
import ruamel.yaml
from . import _, common, metadata, rewritemeta
from fdroidserver._yaml import yaml
config = None
@ -853,7 +852,7 @@ def lint_config(arg):
passed = False
with path.open() as fp:
data = ruamel.yaml.YAML(typ='safe').load(fp)
data = yaml.load(fp)
common.config_type_check(arg, data)
if path.name == mirrors_name:

View file

@ -31,6 +31,7 @@ from collections import OrderedDict
from . import common
from . import _
from .exception import MetaDataException
from ._yaml import yaml
srclibs = None
warnings_action = None
@ -472,7 +473,6 @@ def parse_yaml_srclib(metadatapath):
with metadatapath.open("r", encoding="utf-8") as f:
try:
yaml = ruamel.yaml.YAML(typ='safe')
data = yaml.load(f)
if type(data) is not dict:
if platform.system() == 'Windows':
@ -709,8 +709,7 @@ def parse_yaml_metadata(mf):
"""
try:
yaml = ruamel.yaml.YAML(typ='safe')
yamldata = yaml.load(mf)
yamldata = common.yaml.load(mf)
except ruamel.yaml.YAMLError as e:
_warn_or_exception(
_("could not parse '{path}'").format(path=mf.name)
@ -1249,19 +1248,24 @@ def _app_to_yaml(app):
def write_yaml(mf, app):
"""Write metadata in yaml format.
This requires the 'rt' round trip dumper to maintain order and needs
custom indent settings, so it needs to instantiate its own YAML
instance. Therefore, this function deliberately avoids using any of
the common YAML parser setups.
Parameters
----------
mf
active file discriptor for writing
app
app metadata to written to the yaml file
app metadata to written to the YAML file
"""
_del_duplicated_NoSourceSince(app)
yaml_app = _app_to_yaml(app)
yaml = ruamel.yaml.YAML()
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.dump(yaml_app, stream=mf)
yamlmf = ruamel.yaml.YAML(typ='rt')
yamlmf.indent(mapping=2, sequence=4, offset=2)
yamlmf.dump(yaml_app, stream=mf)
def write_metadata(metadatapath, app):