Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Depend on `diskcache-community` instead of `diskcache`, which is unmaintained and flagged for CVE-2025-69872. `fetch_remote_url` reads its cache through `SafeJSONDisk`, which reads the existing cache files but never unpickles anything.
- Support `flows.yaml` files (`moz://mozilla.org/schemas/glean/flows/1-0-0`), declaring multi-step tasks measured as a unit over existing event metrics. Flows are validated against the metrics passed to the parser, with warnings for events that aren't defined in any of them (`UNRESOLVED_FLOW_EVENT`) and for flows with no exit events (`EMPTY_ENDS_ON`). Kotlin, Swift, and Rust output gains the flow definitions to pass to Glean at initialization (`GleanFlows`), and Markdown output documents flows and the flows each event belongs to. Flows are passed to external translators as the `flows` option ([bug 2077133](https://bugzilla.mozilla.org/show_bug.cgi?id=2077133))
- Fix tests that failed when `SOURCE_DATE_EPOCH` is set in the environment
- Add a `skip_schema_validation` option to `parse_objects` to skip schema validation of input files ([bug 2076540](https://bugzilla.mozilla.org/show_bug.cgi?id=2076540))
Expand Down
16 changes: 11 additions & 5 deletions glean_parser/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Any, Callable, Iterable, Sequence, Tuple, Union, Optional
import urllib.request

import diskcache # type: ignore
import diskcache_community as diskcache
import jinja2
import jsonschema # type: ignore
from jsonschema import _utils # type: ignore
Expand Down Expand Up @@ -283,6 +283,11 @@ def resolve_remote(self, uri):
return NullResolver.from_schema(schema)


# SafeJSONDisk reads the same files as JSONDisk but never unpickles anything,
# and on_error="miss" turns an unreadable entry into a cache miss.
_CACHE_DISK = diskcache.SafeJSONDisk.with_options(on_error="miss")


def fetch_remote_url(url: str, cache: bool = True):
"""
Fetches the contents from an HTTP url or local file path, and optionally
Expand All @@ -300,14 +305,15 @@ def fetch_remote_url(url: str, cache: bool = True):

if cache:
cache_dir = platformdirs.user_cache_dir("glean_parser", "mozilla")
with diskcache.Cache(cache_dir, disk=diskcache.JSONDisk) as dc:
if key in dc:
return dc[key]
with diskcache.Cache(cache_dir, disk=_CACHE_DISK) as dc:
contents = dc.get(key)
if contents is not None:
return contents

contents = urllib.request.urlopen(url).read().decode("utf-8")

if cache:
with diskcache.Cache(cache_dir, disk=diskcache.JSONDisk) as dc:
with diskcache.Cache(cache_dir, disk=_CACHE_DISK) as dc:
dc[key] = contents

return contents
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ classifiers = [
requires-python = ">=3.9"
dependencies = [
"Click>=7",
"diskcache>=4",
"diskcache-community>=5.8.2",
"Jinja2>=2.10.1",
"jsonschema>=3.0.2",
"platformdirs>=2.4.0",
Expand Down
68 changes: 68 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
# Any copyright is dedicated to the Public Domain.
# http://creativecommons.org/publicdomain/zero/1.0/

import io
import os
import pickle
import sqlite3

import diskcache_community as diskcache

from glean_parser import util
from glean_parser.util import to_camel_case, remove_output_params


Expand Down Expand Up @@ -39,3 +47,63 @@ def test_removing_output_params():
test = remove_output_params(d, "defined_in")

assert test == output_removed


def _run_planted(path):
with open(path, "w") as fd:
fd.write("ran")
return "planted"


class _Planted:
def __init__(self, path):
self.path = path

def __reduce__(self):
return (_run_planted, (self.path,))


def test_fetch_remote_url_cache(tmp_path, monkeypatch):
cache_dir = tmp_path / "cache"
monkeypatch.setattr(util.platformdirs, "user_cache_dir", lambda *a: str(cache_dir))
calls = []

def urlopen(url):
calls.append(url)
return io.BytesIO(b"schema contents")

monkeypatch.setattr(util.urllib.request, "urlopen", urlopen)
url = "https://example.com/schema.json"

assert util.fetch_remote_url(url) == "schema contents"
assert util.fetch_remote_url(url) == "schema contents"
assert len(calls) == 1

# A row rewritten by someone else to hold a pickle is a miss, not code
# that runs: the contents are fetched again and the pickle never loads.
marker = tmp_path / "marker"
con = sqlite3.connect(os.path.join(str(cache_dir), "cache.db"))
con.execute(
"UPDATE Cache SET mode = 4, value = ?",
(pickle.dumps(_Planted(str(marker)), protocol=2),),
)
con.commit()
con.close()

assert util.fetch_remote_url(url) == "schema contents"
assert len(calls) == 2
assert not marker.exists()


def test_fetch_remote_url_reads_existing_jsondisk_cache(tmp_path, monkeypatch):
cache_dir = tmp_path / "cache"
monkeypatch.setattr(util.platformdirs, "user_cache_dir", lambda *a: str(cache_dir))
monkeypatch.setattr(util.urllib.request, "urlopen", None)
url = "https://example.com/schema.json"

# Entries written by earlier releases, through JSONDisk, are still read.
key = (url, str(util.sys.version_info))
with diskcache.Cache(str(cache_dir), disk=diskcache.JSONDisk) as dc:
dc[key] = "cached earlier"

assert util.fetch_remote_url(url) == "cached earlier"
12 changes: 6 additions & 6 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.