44import re
55import tempfile
66from pathlib import Path
7+ from urllib .parse import urlsplit
78
89
910REQUIRED_ROLES = ("previous" , "target" )
1415 "pg_version_num" ,
1516 "postgres_sha256" ,
1617)
18+ COMMIT_PATTERN = re .compile (r"^[0-9a-f]{40}$" )
19+ PG_VERSION_PATTERN = re .compile (r"^[0-9]+(?:\.[0-9]+)+$" )
20+ SHA256_PATTERN = re .compile (r"^[0-9a-f]{64}$" )
21+
22+
23+ def _require_nonempty_string (value , name ):
24+ if not isinstance (value , str ) or not value .strip ():
25+ raise ValueError (f"{ name } must be a non-empty string" )
26+
27+
28+ def _validate_version_pin (version , role ):
29+ prefix = f"versions.{ role } "
30+ for field in REQUIRED_VERSION_FIELDS :
31+ if field not in version :
32+ raise ValueError (f"missing required pin: { prefix } .{ field } " )
33+
34+ _require_nonempty_string (version ["branch" ], f"{ prefix } .branch" )
35+ _require_nonempty_string (version ["pg_version" ], f"{ prefix } .pg_version" )
36+
37+ if not isinstance (version ["commit" ], str ) or not COMMIT_PATTERN .fullmatch (
38+ version ["commit" ]
39+ ):
40+ raise ValueError (f"{ prefix } .commit must be 40 lowercase hexadecimal characters" )
41+ if not PG_VERSION_PATTERN .fullmatch (version ["pg_version" ]):
42+ raise ValueError (f"{ prefix } .pg_version must use numeric dotted components" )
43+ if isinstance (version ["pg_version_num" ], bool ) or not isinstance (
44+ version ["pg_version_num" ], int
45+ ):
46+ raise ValueError (f"{ prefix } .pg_version_num must be an integer" )
47+ if not isinstance (
48+ version ["postgres_sha256" ], str
49+ ) or not SHA256_PATTERN .fullmatch (version ["postgres_sha256" ]):
50+ raise ValueError (
51+ f"{ prefix } .postgres_sha256 must be 64 lowercase hexadecimal characters"
52+ )
1753
1854
1955def load_pins (path ):
@@ -23,6 +59,10 @@ def load_pins(path):
2359
2460 if not isinstance (pins , dict ) or "libpg_query_url" not in pins :
2561 raise ValueError ("missing required pin: libpg_query_url" )
62+ _require_nonempty_string (pins ["libpg_query_url" ], "libpg_query_url" )
63+ parsed_url = urlsplit (pins ["libpg_query_url" ])
64+ if not parsed_url .scheme or not parsed_url .netloc :
65+ raise ValueError ("libpg_query_url must be a non-empty absolute URL" )
2666
2767 versions = pins .get ("versions" )
2868 if not isinstance (versions , dict ):
@@ -32,9 +72,7 @@ def load_pins(path):
3272 version = versions .get (role )
3373 if not isinstance (version , dict ):
3474 raise ValueError (f"missing required pin: versions.{ role } " )
35- for field in REQUIRED_VERSION_FIELDS :
36- if field not in version :
37- raise ValueError (f"missing required pin: versions.{ role } .{ field } " )
75+ _validate_version_pin (version , role )
3876
3977 return pins
4078
0 commit comments