#!/usr/bin/python3 -su

## Copyright (C) 2024 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## A very simple JSON parser that extracts the "version" string from Tor
## Browser update information JSON. Assumes the input JSON could be malicious,
## and is thus hardened against unexpected data.

from pathlib import Path
import argparse
import json
import sys

# Prints a message to stdout unless running in quiet mode.
def print_noisy(msg):
    if args.quiet:
        return
    print(msg, file=sys.stderr)

def tbbversion_x64():
    try:
        input_text = input_path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        print_noisy("ERROR: Rejecting input file, input is not valid UTF-8!")
        sys.exit(3)
    except Exception:
        print_noisy("ERROR: Cannot read input file!")
        sys.exit(2)

    try:
        input_json = json.loads(input_text)
    except Exception:
        print_noisy("ERROR: JSON parsing failed!")
        sys.exit(3)

    if type(input_json) is not dict:
        print_noisy("ERROR: Rejecting input JSON, did not deserialize to a Python dict!")
        sys.exit(3)

    version_string = input_json.get("version")
    if type(version_string) is not str:
        print_noisy("ERROR: Rejecting input JSON, version object did not deserialize to a Python string!")
        sys.exit(3)

    ## In the event the parser is compromised, this check can't be relied on,
    ## but it harms nothing to include it. Anything that reads from the output
    ## file MUST redo this check.
    if len(version_string) > 20:
        print_noisy("ERROR: Rejecting input JSON, version string is abnormally long!")
        sys.exit(3)

    ## Not necessary for security, but useful for debugging.
    try:
        version_string.encode(encoding="utf-8")
    except UnicodeEncodeError:
        print_noisy("ERROR: Rejecting input JSON, version string contains invalid Unicode!")
        sys.exit(3)

    try:
        with output_path.open(mode = 'w') as f:
            f.writelines([version_string])
    except Exception:
        print_noisy("ERROR: Could not write version string to output file!")
        sys.exit(2)

    sys.exit(0)

argument_parser = argparse.ArgumentParser(prog = "version-parser",
                                          description = "Parses JSON files to find the latest version of Tor Browser.")
argument_parser.add_argument("input_file",
                             action = "store",
                             help = "The JSON file to use as input.")
argument_parser.add_argument("output_file",
                             action = "store",
                             help = "The file to save the latest version of the Tor Browser in, if found.")
argument_parser.add_argument("-q",
                             "--quiet",
                             action = "store_true",
                             help = "Gets rid of verbose status output.")
args = argument_parser.parse_args()

if args.input_file is None:
    print_noisy("ERROR: No input file provided.")
    sys.exit(1)
elif args.output_file is None:
    print_noisy("ERROR: No output file provided.")
    sys.exit(1)

input_path = Path(args.input_file).resolve(strict=False)
output_path = Path(args.output_file).resolve(strict=False)

if not input_path.as_posix().startswith("/tmp/"):
    print_noisy("ERROR: The input file must be under /tmp.")
    sys.exit(2)
if not output_path.as_posix().startswith("/tmp/"):
    print_noisy("ERROR: The output file must be under /tmp.")
    sys.exit(2)

if not input_path.is_file():
    print_noisy("ERROR: Input file does not exist or is not a regular file.")
    sys.exit(2)
else:
    try:
        output_path.touch(mode=0o644, exist_ok=True)
    except Exception:
        print_noisy("ERROR: Cannot access output file.")
        sys.exit(2)

MAX_INPUT_BYTES = 2 * 1024 * 1024
if input_path.stat().st_size > MAX_INPUT_BYTES:
    print_noisy("ERROR: Rejecting input file, size exceeds 2 MiB!")
    sys.exit(3)

tbbversion_x64()
