-
Notifications
You must be signed in to change notification settings - Fork 61
/
validate-spice
executable file
·220 lines (183 loc) · 9.15 KB
/
validate-spice
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/python3
import argparse
import glob
import json
import os
import sys
import subprocess
from gi.repository import GLib
from PIL import Image
SPICE_EXT = ".nemo_action.in"
GROUP = "Nemo Action"
class CheckError(Exception):
pass
# function with checks for an xlet
def validate_xlet(uuid):
valid = False
os.chdir(uuid)
try:
# Check mandatory files
for file in ["info.json", f"files/{uuid}/icon.png"]:
if not os.path.exists(file):
raise CheckError(f"Missing file: {file}")
for _, _, files in os.walk(f'files/{uuid}'):
if (any(ext.endswith('.po') for ext in files) and not
any(ext.endswith('.pot') for ext in files)):
raise CheckError(f"Invalid location for translation files! (Move any .po or .pot to files/{uuid}/po)")
if any(ext.endswith(".mo") for ext in files):
raise CheckError(f"Translation files should not be compiled! (Delete any .mo files in {uuid})")
if f"{uuid}{SPICE_EXT}" not in os.listdir('.'):
raise CheckError(f"Missing main {SPICE_EXT} file")
# Check if there are any improperly placed files
for file in glob.glob("*"):
if file.endswith(".po") or file.endswith(".pot"):
raise CheckError(f"[{uuid}] Invalid location for translation files!")
# Check if there is an improperly duplicated file
if len(glob.glob("files/*/po/*.pot")) > 1:
raise CheckError(f"[{uuid}] Too many .pot files!")
# Check if there are any improperly named file(s)
for file in glob.glob("files/*/po/*"):
if not (file.endswith(".po") or file.endswith(".pot")):
raise CheckError(f"[{uuid}] Invalid file found in translation directory: {file}")
# Check forbidden .nemo_action.in format
keyfile = GLib.KeyFile.new()
try:
keyfile.load_from_file(f"{uuid}{SPICE_EXT}", GLib.KeyFileFlags.KEEP_TRANSLATIONS)
# verify if the file has at least one section inside...
if keyfile.get_groups().length < 1:
raise CheckError(f"Missing {GROUP} section in {SPICE_EXT} file!")
if keyfile.get_groups().length > 1:
raise CheckError(f"Too many sections in {SPICE_EXT} file!")
if keyfile.get_groups()[0][0] != GROUP:
raise CheckError(f"Invalid section in {SPICE_EXT} file!\nRequired: [{GROUP}]")
get_keys = keyfile.get_keys(GROUP)[0]
all_keys = set(keyfile.get_keys(GROUP)[0])
if len(get_keys) != len(all_keys):
raise CheckError(f"Duplicate keys detected in {SPICE_EXT} file!")
if {'Extensions', 'Mimetypes'}.issubset(all_keys):
raise CheckError(f"Too many keys: Either 'Extensions' OR 'Mimetypes' in {SPICE_EXT} file!")
if 'Extensions' not in all_keys and 'Mimetypes' not in all_keys:
raise CheckError(f"Missing key: Either 'Extensions' OR 'Mimetypes' are required in {SPICE_EXT} file!")
bad_chars = ['[', ']']
for key in all_keys:
if any(bad_char in key for bad_char in bad_chars):
raise CheckError(f"Bad key: '{key}' in {SPICE_EXT} file!")
comment_key = "_Comment"
base_keys = ["_Name", comment_key, "Exec", "Selection"]
if 'Conditions' in all_keys and 'desktop' in keyfile.get_value(GROUP, 'Conditions').split(';'):
if comment_key in all_keys:
raise CheckError(f"Unnecessary key: '{comment_key}' in {SPICE_EXT} file!")
base_keys.remove(comment_key)
for key in base_keys:
if key.startswith('_') and '"' in keyfile.get_value(GROUP, key):
raise CheckError(f"Invalid character '\"' in '{key}' in {SPICE_EXT} file!")
if key not in all_keys:
raise CheckError(f"Missing key: '{key}' not in {SPICE_EXT} file!")
except GLib.Error as exc:
raise CheckError(f"Unable to read: {uuid}{SPICE_EXT}") from exc
# Check forbidden files
for file in ["icon.png", "screenshot.png", f"{uuid}.nemo_action"]:
if os.path.exists(file):
raise CheckError(f"Forbidden file: {file}")
# Check mandatory directories
for directory in ["files", f"files/{uuid}"]:
if not os.path.isdir(directory):
raise CheckError(f"Missing directory: {directory}")
# Check that there are no files in files other than the uuid directory
if len(os.listdir("files")) != 1:
raise CheckError("The files directory should ONLY contain the $uuid directory!")
# check info.json
with open("info.json", encoding='utf-8') as file:
try:
info = json.load(file)
except Exception as e:
raise CheckError(f"Could not parse info.json: {e}") from e
# check mandatory fields
for field in ['author']:
if field not in info:
raise CheckError(f"Missing field '{field}' in info.json")
# check metadata.json
with open(f"files/{uuid}/metadata.json", encoding='utf-8') as file:
try:
metadata = json.load(file)
except Exception as e:
raise CheckError(f"Could not parse metadata.json: {e}") from e
# check forbidden fields
for field in ['icon', 'dangerous', 'last-edited', 'max-instances']:
if field in metadata:
raise CheckError(f"Forbidden field '{field}' in {file.name}")
# check mandatory fields
for field in ['uuid', 'name', 'description']:
if field not in metadata:
raise CheckError(f"Missing field '{field}' in {file.name}")
# check uuid value
if metadata['uuid'] != uuid:
raise CheckError(f"Wrong uuid in {file.name}")
# check for unicode characters in metadata
for field in metadata:
strvalue = str(metadata[field])
if len(strvalue.encode()) != len(strvalue):
raise CheckError(f"Forbidden unicode characters in field '{field}' in {file.name}")
# check if icon is square
im = Image.open(f"files/{uuid}/icon.png")
(width, height) = im.size
if width != height:
raise CheckError("icon.png has to be square.")
# check po and pot files
podir = f"files/{uuid}/po"
if os.path.isdir(podir):
for file in os.listdir(podir):
if file.endswith(".po"):
pocheck = subprocess.run(["msgfmt", "-o", "-", "-c",
os.path.join(podir, file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True, check=True)
if pocheck.stderr:
raise CheckError(f"The following errors were found in translation file '{file}':\n{pocheck.stderr}"
f"HINT: Most errors in translation file `{file}` can usually be prevented and solved by using Poedit.\n")
elif file.endswith(".pot"):
potcheck = subprocess.run(["xgettext", "-o", "-",
os.path.join(podir, file)],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True, check=True)
if potcheck.stderr:
raise CheckError(f"The following errors were found in translation template '{file}':\n{potcheck.stderr}")
# Finally...
valid = True
except CheckError as error:
print(f"[{uuid}] Error during validation: {error}")
except Exception as error:
print(f"[{uuid}] Unknown error. {error}")
os.chdir("..")
return valid
def quit(valid):
if valid:
print("No errors found. Everything looks good.")
sys.exit(0)
else:
print("\nError(s) found! Once you fix the issue(s), please re-run "
"'validate-spice <uuid>' to check for further errors.")
sys.exit(1)
def main():
parser = argparse.ArgumentParser()
parser.description = 'Arguments for validate-spice'
parser.add_argument('-a', '--all', action='store_true',
help='Validate all Spices')
parser.add_argument('uuid', type=str, metavar='UUID', nargs='?',
help='the UUID of the Spice')
args = parser.parse_args()
is_valid = []
if args.all:
for file_path in os.listdir("."):
if os.path.isdir(file_path) and not file_path.startswith("."):
is_valid.append(validate_xlet(file_path))
elif args.uuid:
is_valid.append(validate_xlet(args.uuid.rstrip('/')))
else:
parser.print_help()
sys.exit(2)
quit(all(is_valid))
if __name__ == "__main__":
main()