#!/usr/bin/env python3 # # SPDX-FileCopyrightText: 2023-2026 Sébastien Helleu # # SPDX-License-Identifier: GPL-3.0-or-later # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """Check Curl symbols defined in WeeChat source code. Check if Curl symbols defined in src/core/core-url.c are matching symbols defined in Curl (introduced/deprecated/last versions), using this file: https://github.com/curl/curl/blob/master/docs/libcurl/symbols-in-versions. File symbols-in-versions must be passed as stdin to the script. Usage example: ``` URL=https://raw.githubusercontent.com/curl/curl/master/docs/libcurl/symbols-in-versions curl $URL | ./check_curl_symbols.py ``` Or with Curl repository cloned locally: ``` ./check_curl_symbols.py < /path/to/curl/docs/libcurl/symbols-in-versions ``` This script requires Python 3.7+. """ # ruff: noqa: COM812,T201 import io import re import sys from dataclasses import dataclass from pathlib import Path SRC_PATH = Path(__file__).resolve().parent.parent / "src" / "core" / "core-url.c" # NOTE: keep version in sync with CMakeLists.txt CURL_MIN_VERSION_STR = "7.68.0" WEECHAT_CURL_MIN_VERSION_RE = ( r"#if LIBCURL_VERSION_NUM >= (?P0x[0-9A-F]+) " r"/\* (?P[0-9][0-9.]+) \*/" ) WEECHAT_CURL_MAX_VERSION_RE = ( r"#if LIBCURL_VERSION_NUM < (?P0x[0-9A-F]+) " r"/\* < (?P[0-9][0-9.]+) \*/" ) WEECHAT_CURL_MIN_MAX_VERSION_RE = ( r"#if LIBCURL_VERSION_NUM >= (?P0x[0-9A-F]+) " r"&& LIBCURL_VERSION_NUM < (?P0x[0-9A-F]+) " r"/\* (?P[0-9][0-9.]+) < " r"(?P[0-9][0-9.]+) \*/" ) WEECHAT_ENDIF_RE = r"#endif" WEECHAT_CURL_CONSTANT_RE = r" URL_DEF_CONST\((?P[A-Z0-9_]+), (?P[A-Z0-9_]+)\)," WEECHAT_CURL_OPTION_RE = r" URL_DEF_OPTION\((?P[A-Z0-9_]+), .*\)," CURL_SYMBOL_RE = r"[A-Z][A-Z0-9_]" CURL_VERSION_ITEMS = 3 @dataclass class WeechatCurlSymbol: """A Curl symbol declared in WeeChat.""" name: str min_curl: int = 0 max_curl: int = 0 line_no: int = 0 def curl_version_to_int(version: str) -> int: """Convert Curl version as string to integer. :param version: version as string (eg: "7.87.0") :return: version as integer, eg: 481024 (== 0x075700, 0x57 == 87) """ if version == "-": return 0 result = 0 items = version.split(".") while len(items) < CURL_VERSION_ITEMS: items.append("0") factor = 0 for item in reversed(items): result += int(item) << factor factor += 8 return result def curl_version_to_str(version: int) -> str: """Convert Curl version as integer to string. :param version: version as integer, eg: 481024 (0x075700) :return: version as string, eg: "7.87.0" """ if version == 0: return "-" result = "" while version > 0: result = str(version & 0xFF) + "." + result version = version >> 8 return result.rstrip(".") def get_curl_symbols(symbols_file: io.TextIOBase) -> dict[str, tuple[int, int]]: """Parse file docs/libcurl/symbols-in-versions from Curl repository. :param symbols_file: file with Curl symbols :return: Curl symbols as dict: {name: (version_min, version_max)} """ curl_symbol_pattern = re.compile(CURL_SYMBOL_RE) symbols: dict[str, tuple[int, int]] = {} if symbols_file.isatty(): return symbols for line in symbols_file: match = re.match(curl_symbol_pattern, line) if match: name, intro, deprec, last, *_ = (line + " - -").split() v_max = last if last != "-" else deprec symbols[name] = ( curl_version_to_int(intro), curl_version_to_int(v_max), ) return symbols def check_req_symbols(symbols: list[WeechatCurlSymbol]) -> int: """Check the symbols' min/max version, relative to min Curl version required. :return: errors """ errors: int = 0 req_curl = curl_version_to_int(CURL_MIN_VERSION_STR) for symbol in symbols: if symbol.min_curl and symbol.min_curl <= req_curl: print( f"{SRC_PATH}:{symbol.line_no}: min version for " f"symbol {symbol.name} older than minimal required " f"curl {req_curl}. Remove if guard." ) errors += 1 if symbol.max_curl and symbol.max_curl < req_curl: print( f"{SRC_PATH}:{symbol.line_no}: max version for " f"symbol {symbol.name} older than minimal required " f"curl {req_curl}. Remove the symbol." ) errors += 1 return errors def get_weechat_curl_symbols() -> tuple[list[WeechatCurlSymbol], int]: # noqa: C901,PLR0915 """Parse Curl symbols declared in src/core/core-url.c. :return: tuple (list_symbols, errors) """ min_version_pattern = re.compile(WEECHAT_CURL_MIN_VERSION_RE) max_version_pattern = re.compile(WEECHAT_CURL_MAX_VERSION_RE) min_max_version_pattern = re.compile(WEECHAT_CURL_MIN_MAX_VERSION_RE) endif_pattern = re.compile(WEECHAT_ENDIF_RE) constant_pattern = re.compile(WEECHAT_CURL_CONSTANT_RE) option_pattern = re.compile(WEECHAT_CURL_OPTION_RE) v_min: int = 0 v_max: int = 0 symbols: list[WeechatCurlSymbol] = [] errors: int = 0 line_no: int = 0 with Path(SRC_PATH).open(encoding="utf-8") as src_file: for line in src_file: line_no += 1 # min Curl version match = re.match(min_version_pattern, line) if match: hex_min_vers = match["hex_min_version"] str_min_vers = match["str_min_version"] v_min, v_max = int(hex_min_vers, 0), 0 comment_min = curl_version_to_int(str_min_vers) if v_min != comment_min: print( f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}" ) errors += 1 continue # max Curl version match = re.match(max_version_pattern, line) if match: hex_max_vers = match["hex_max_version"] str_max_vers = match["str_max_version"] v_min, v_max = 0, int(hex_max_vers, 0) comment_max = curl_version_to_int(str_max_vers) if v_max != comment_max: print( f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}" ) errors += 1 continue # min + max Curl version match = re.match(min_max_version_pattern, line) if match: hex_min_vers = match["hex_min_version"] hex_max_vers = match["hex_max_version"] str_min_vers = match["str_min_version"] str_max_vers = match["str_max_version"] v_min, v_max = int(hex_min_vers, 0), int(hex_max_vers, 0) comment_min = curl_version_to_int(str_min_vers) comment_max = curl_version_to_int(str_max_vers) if v_min != comment_min: print( f"{SRC_PATH}:{line_no}: min version not matching the comment: {hex_min_vers} != {str_min_vers}" ) errors += 1 if v_max != comment_max: print( f"{SRC_PATH}:{line_no}: max version not matching the comment: {hex_max_vers} != {str_max_vers}" ) errors += 1 continue # end of min/max Curl version match = re.match(endif_pattern, line) if match: v_min, v_max = 0, 0 continue # Curl constant match = re.match(constant_pattern, line) if match: name = f"CURL{match['prefix']}_{match['name']}" symbols.append(WeechatCurlSymbol(name, v_min, v_max, line_no)) continue # Curl option match = re.match(option_pattern, line) if match: name = f"CURLOPT_{match['name']}" symbols.append(WeechatCurlSymbol(name, v_min, v_max, line_no)) continue errors += check_req_symbols(symbols) return symbols, errors def check_symbols( weechat_curl_symbols: list[WeechatCurlSymbol], curl_symbols: dict[str, tuple[int, int]], ) -> int: """Check that symbols declared are matching Curl symbols. :param weechat_curl_symbols: list of Curl symbols in WeeChat :param curl_symbols: list of all Curl symbols """ req_curl = curl_version_to_int(CURL_MIN_VERSION_STR) to_str = curl_version_to_str errors = 0 for symbol in weechat_curl_symbols: curl_symbol = curl_symbols.get(symbol.name) if not curl_symbol: print(f"{SRC_PATH}:{symbol.line_no}: symbol {symbol.name} not found in Curl") errors += 1 continue if curl_symbol[0] > req_curl and symbol.min_curl != curl_symbol[0]: print( f"{SRC_PATH}:{symbol.line_no}: min version for " f"symbol {symbol.name} differs: " f"{to_str(symbol.min_curl)} in WeeChat, " f"{to_str(curl_symbol[0])} in Curl" ) errors += 1 if curl_symbol[1] >= req_curl and symbol.max_curl != curl_symbol[1]: print( f"{SRC_PATH}:{symbol.line_no}: max version for " f"symbol {symbol.name} differs: " f"{to_str(symbol.max_curl)} in WeeChat, " f"{to_str(curl_symbol[1])} in Curl" ) errors += 1 return errors def main() -> int: """Check Curl symbols and return the number of errors found.""" curl_symbols = get_curl_symbols(sys.stdin) if not curl_symbols: sys.exit("FATAL: failed to read Curl symbols on standard input") weechat_curl_symbols, errors = get_weechat_curl_symbols() errors += check_symbols(weechat_curl_symbols, curl_symbols) dict_err = {0: "all good!", 1: "1 error"} print("Curl symbols:", dict_err.get(errors, f"{errors} errors")) return errors if __name__ == "__main__": sys.exit(min(main(), 255))