#!/usr/bin/env python3 # Parse Steam/Source VDF Files # Reference: https://developer.valvesoftware.com/wiki/KeyValues#File_Format # (c) 2015-2020 Taeyeon Mori; CC-BY-SA from __future__ import unicode_literals import io from typing import Dict, Union DeepDict = Dict[str, Union[str, "DeepDict"]] class VdfParser: """ Simple Steam/Source VDF parser """ # Special Characters quote_char = "\"" escape_char = "\\" begin_char = "{" end_char = "}" whitespace_chars = " \t\n" comment_char = "/" newline_char = "\n" def __init__(self, *, encoding=False, factory=dict, strict=True): """ @brief Construct a VdfParser instance @param encoding Encoding for bytes operations. Pass None to use unicode strings @param factory A factory function creating a mapping type from an iterable of key/value tuples. """ self.encoding = encoding if encoding: self.empty_string = self.empty_string.encode(encoding) self.quote_char = self.quote_char.encode(encoding) self.escape_char = self.escape_char.encode(encoding) self.begin_char = self.begin_char.encode(encoding) self.end_char = self.end_char.encode(encoding) self.whitespace_chars = self.whitespace_chars.encode(encoding) self.comment_char = self.comment_char.encode(encoding) self.newline_char = self.newline_char.encode(encoding) self.factory = factory self.strict = strict def _make_map(self, tokens): return self.factory(zip(tokens[::2], tokens[1::2])) def _parse_map(self, fd, inner=False): tokens = [] current = [] escape = False quoted = False comment = False if self.encoding: make_string = b"".join else: make_string = "".join def finish(override=False): if current or override: tokens.append(make_string(current)) current.clear() while True: c = fd.read(1) if not c: finish() if len(tokens) / 2 != len(tokens) // 2: raise ValueError("Unexpected EOF: Last pair incomplete") elif self.strict and (escape or quoted or inner): raise ValueError("Unexpected EOF: EOF encountered while not processing outermost mapping") return self._make_map(tokens) if escape: current.append(c) escape = False elif quoted: if c == self.escape_char: escape = True elif c == self.quote_char: quoted = False finish(override=True) else: current.append(c) elif comment: if c == self.newline_char: comment = False else: if c == self.escape_char: escape = True elif c == self.begin_char: finish() if len(tokens) / 2 == len(tokens) // 2 and (self.strict or self.factory == dict): raise ValueError("Sub-dictionary cannot be a key") tokens.append(self._parse_map(fd, True)) elif c == self.end_char: finish() if len(tokens) / 2 != len(tokens) // 2: raise ValueError("Unexpected close: Missing last value (Unbalanced tokens)") return self._make_map(tokens) elif c in self.whitespace_chars: finish() elif c == self.quote_char: finish() quoted = True elif c == self.comment_char and current and current[-1] == self.comment_char: del current[-1] finish() comment = True else: current.append(c) def parse(self, fd) -> DeepDict: """ Parse a VDF file into a python dictionary """ return self._parse_map(fd) def parse_string(self, content) -> DeepDict: """ Parse the content of a VDF file """ if self.encoding: return self.parse(io.BytesIO(content)) else: return self.parse(io.StringIO(content)) def _make_literal(self, lit): # TODO return "\"%s\"" % (str(lit).replace("\\", "\\\\").replace("\"", "\\\"")) def _write_map(self, fd, dictionary, indent): if indent is None: def write(str=None, i=False, d=False, nl=False): if str: fd.write(str) if d: fd.write(" ") else: def write(str=None, i=False, d=False, nl=False): if not str and nl: fd.write("\n") else: if i: fd.write("\t" * indent) if str: fd.write(str) if nl: fd.write("\n") elif d: fd.write("\t\t") for k, v in dictionary.items(): if isinstance(v, dict): write(self._make_literal(k), i=1, d=1, nl=1) write("{", i=1, nl=1) self._write_map(fd, v, indent + 1 if indent is not None else None) write("}", i=1) else: write(self._make_literal(k), i=1, d=1) write(self._make_literal(v)) write(d=1, nl=1) def write(self, fd, dictionary: DeepDict, *, pretty=True): """ Write a dictionary to a file in VDF format """ if self.encoding: raise NotImplementedError("Writing in binary mode is not implemented yet.") # TODO (maybe) self._write_map(fd, dictionary, 0 if pretty else None)