-
Notifications
You must be signed in to change notification settings - Fork 1
/
vdf.py
206 lines (154 loc) · 5.34 KB
/
vdf.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python
# a simple parser for Valve's KeyValue format
# https://developer.valvesoftware.com/wiki/KeyValues
#
# author: Rossen Popov, 2014
#
# use at your own risk
import re
from codecs import BOM, BOM_BE, BOM_LE, BOM_UTF8, BOM_UTF16, BOM_UTF16_BE, BOM_UTF16_LE, BOM_UTF32, BOM_UTF32_BE, BOM_UTF32_LE
BOMS = [
BOM,
BOM_BE,
BOM_LE,
BOM_UTF8,
BOM_UTF16,
BOM_UTF16_BE,
BOM_UTF16_LE,
BOM_UTF32,
BOM_UTF32_BE,
BOM_UTF32_LE,
]
###############################################
#
# Takes a file or str and returns dict
#
# Function assumes valid VDF as input.
# Invalid VDF will result in unexpected output
#
###############################################
def parse(a):
a_type = type(a)
lines = open(a).readlines() #.readlines()
# check first line BOM and remove
for bom in BOMS:
if lines[0][:len(bom)] == bom:
lines[0] = lines[0][len(bom):]
break;
# init
obj = dict()
stack = [obj]
expect_bracket = False
name = ""
re_keyvalue = re.compile(r'^"((?:\\.|[^\\"])*)"[ \t]*"((?:\\.|[^\\"])*)(")?')
re_key = re.compile(r'^"((?:\\.|[^\\"])*)"')
itr = iter(lines)
for line in itr:
line = line.strip()
# skip empty and comment lines
if line == "" or line[0] == '/':
continue
# one level deeper
if line[0] == "{":
expect_bracket = False
continue
if expect_bracket:
raise SyntaxError("vdf.parse: invalid syntax")
# one level back
if line[0] == "}":
stack.pop()
continue
# parse keyvalue pairs
if line[0] == '"':
while True:
m = re_keyvalue.match(line)
# we've matched a simple keyvalue pair, map it to the last dict obj in the stack
if m:
# if the value is line consume one more line and try to match again, until we get the KeyValue pair
if m.group(3) == None:
line += "\n" + next(itr)
continue
stack[-1][m.group(1)] = m.group(2)
# we have a key with value in parenthesis, so we make a new dict obj (one level deep)
else:
m = re_key.match(line)
if not m:
raise SyntaxError("vdf.parse: invalid syntax")
key = m.group(1)
stack[-1][key] = dict()
stack.append(stack[-1][key])
expect_bracket = True
# exit the loop
break
if len(stack) != 1:
raise SyntaxError("vdf.parse: unclosed parenthasis or quotes")
return obj
###############################################
#
# Take a dict, reuturns VDF in str buffer
#
# dump(dict(), pretty=True) for indented VDF
#
###############################################
def dump(a, **kwargs):
pretty = kwargs.get("pretty", False)
if type(pretty) is not bool:
raise ValueError("Pretty option is a boolean")
return _dump(a,pretty)
def _dump(a,pretty=False,level=0):
if type(a) is not dict:
raise ValueError("Expected parametar to be dict")
indent = "\t"
buf = ""
line_indent = ""
if pretty:
line_indent = indent * level
for key in a:
if type(a[key]) is dict:
buf += '%s"%s"\n%s{\n%s%s}\n' % (line_indent, key, line_indent, _dump(a[key],pretty,level+1), line_indent)
else:
buf += '%s"%s" "%s"\n' % (line_indent, key, str(a[key]))
return buf
###############################################
#
# Testing initiative
#
###############################################
def test():
tests = [
# empty test
[ '' , {} ],
[ {} , '' ],
# simple key and values
[ {1:1}, '"1" "1"\n'],
[ {"a":"1","b":"2"} , '"a" "1"\n"b" "2"\n' ],
# nesting
[ {"a":{"b":{"c":{"d":"1","e":"2"}}}} , '"a"\n{\n"b"\n{\n"c"\n{\n"e" "2"\n"d" "1"\n}\n}\n}\n' ],
[ '"a"\n{\n"b"\n{\n"c"\n{\n"e" "2"\n"d" "1"\n}\n}\n}\n"b" "2"' , {"a":{"b":{"c":{"d":"1","e":"2"}}},"b":"2"} ],
# ignoring comment lines
[ "//comment text\n//comment", {} ],
[ '"a" "b" //comment text', {"a":"b"} ],
[ '//comment\n"a" "1"\n"b" "2" //comment' , {"a":"1","b":"2"} ],
[ '"a"\n{//comment\n}//comment' , {"a":{}} ],
[ '"a" //comment\n{\n}' , {"a":{}} ],
# new linesi n value
[ r'"a" "xx\"xxx"', {"a":r'xx\"xxx'} ],
[ '"a" "xx\\"\nxxx"', {"a":'xx\\"\nxxx'} ],
[ '"a" "\n\n\n\n"', {"a":'\n\n\n\n'} ]
]
for test,expected in tests:
out = None
try:
if type(test) is dict:
out = dump(test)
else:
out = parse(test)
except:
print("Test falure (exception):\n\n%s" % str(test))
raise
if expected != out:
print("Test falure (ouput mismatch):\n\n%s" % str(test))
print("\nOutput:\n\n%s" % str(out))
print("\nExpected:\n\n%s\n" % str(expected))
raise Exception("Output differs from expected result")
return True