]> git.lizzy.rs Git - cheatdb.git/blob - app/tasks/minetestcheck/tree.py
Improve MinetestCheck name validation
[cheatdb.git] / app / tasks / minetestcheck / tree.py
1 import os, re
2 from . import MinetestCheckError, ContentType
3 from .config import parse_conf
4
5 basenamePattern = re.compile("^([a-z0-9_]+)$")
6
7 def get_base_dir(path):
8         if not os.path.isdir(path):
9                 raise IOError("Expected dir")
10
11         root, subdirs, files = next(os.walk(path))
12         if len(subdirs) == 1 and len(files) == 0:
13                 return get_base_dir(path + "/" + subdirs[0])
14         else:
15                 return path
16
17
18 def detect_type(path):
19         if os.path.isfile(path + "/game.conf"):
20                 return ContentType.GAME
21         elif os.path.isfile(path + "/init.lua"):
22                 return ContentType.MOD
23         elif os.path.isfile(path + "/modpack.txt") or \
24                         os.path.isfile(path + "/modpack.conf"):
25                 return ContentType.MODPACK
26         elif os.path.isdir(path + "/mods"):
27                 return ContentType.GAME
28         elif os.path.isfile(path + "/texture_pack.conf"):
29                 return ContentType.TXP
30         else:
31                 return ContentType.UNKNOWN
32
33
34 def get_csv_line(line):
35         return [x.strip() for x in line.split(",") if x.strip() != ""]
36
37
38 class PackageTreeNode:
39         def __init__(self, baseDir, relative, author=None, repo=None, name=None):
40                 self.baseDir  = baseDir
41                 self.relative = relative
42                 self.author   = author
43                 self.name        = name
44                 self.repo        = repo
45                 self.meta        = None
46                 self.children = []
47
48                 # Detect type
49                 self.type = detect_type(baseDir)
50                 self.read_meta()
51
52                 if self.type == ContentType.GAME:
53                         if not os.path.isdir(baseDir + "/mods"):
54                                 raise MinetestCheckError(("Game at {} does not have a mods/ folder").format(self.relative))
55                         self.add_children_from_mod_dir(baseDir + "/mods")
56                 elif self.type == ContentType.MOD:
57                         if self.name and not basenamePattern.match(self.name):
58                                 raise MinetestCheckError(("Invalid base name for mod {} at {}, names must only contain a-z0-9_.") \
59                                         .format(self.name, self.relative))
60                 elif self.type == ContentType.MODPACK:
61                         self.add_children_from_mod_dir(baseDir)
62
63
64         def getMetaFilePath(self):
65                 filename = None
66                 if self.type == ContentType.GAME:
67                         filename = "game.conf"
68                 elif self.type == ContentType.MOD:
69                         filename = "mod.conf"
70                 elif self.type == ContentType.MODPACK:
71                         filename = "modpack.conf"
72                 elif self.type == ContentType.TXP:
73                         filename = "texture_pack.conf"
74                 else:
75                         return None
76
77                 return self.baseDir + "/" + filename
78
79
80         def read_meta(self):
81                 result = {}
82
83                 # .conf file
84                 try:
85                         with open(self.getMetaFilePath() or "", "r") as myfile:
86                                 conf = parse_conf(myfile.read())
87                                 for key, value in conf.items():
88                                         result[key] = value
89                 except IOError:
90                         pass
91
92                 # description.txt
93                 if not "description" in result:
94                         try:
95                                 with open(self.baseDir + "/description.txt", "r") as myfile:
96                                         result["description"] = myfile.read()
97                         except IOError:
98                                 pass
99
100                 # depends.txt
101                 if "depends" in result or "optional_depends" in result:
102                         if "depends" in result:
103                                 result["depends"] = get_csv_line(result["depends"])
104
105                         if "optional_depends" in result:
106                                 result["optional_depends"] = get_csv_line(result["optional_depends"])
107
108                 else:
109                         try:
110                                 pattern = re.compile("^([a-z0-9_]+)\??$")
111
112                                 with open(self.baseDir + "/depends.txt", "r") as myfile:
113                                         contents = myfile.read()
114                                         soft = []
115                                         hard = []
116                                         for line in contents.split("\n"):
117                                                 line = line.strip()
118                                                 if pattern.match(line):
119                                                         if line[len(line) - 1] == "?":
120                                                                 soft.append( line[:-1])
121                                                         else:
122                                                                 hard.append(line)
123
124                                         result["depends"] = hard
125                                         result["optional_depends"] = soft
126
127                         except IOError:
128                                 pass
129
130
131                 # Fix games using "name" as "title"
132                 if self.type == ContentType.GAME:
133                         result["title"] = result["name"]
134                         del result["name"]
135
136                 # Calculate Title
137                 if "name" in result and not "title" in result:
138                         result["title"] = result["name"].replace("_", " ").title()
139
140                 # Calculate short description
141                 if "description" in result:
142                         desc = result["description"]
143                         idx = desc.find(".") + 1
144                         cutIdx = min(len(desc), 200 if idx < 5 else idx)
145                         result["short_description"] = desc[:cutIdx]
146
147                 if "name" in result:
148                         self.name = result["name"]
149                         del result["name"]
150
151                 self.meta = result
152
153         def add_children_from_mod_dir(self, dir):
154                 for entry in next(os.walk(dir))[1]:
155                         path = os.path.join(dir, entry)
156                         if not entry.startswith('.') and os.path.isdir(path):
157                                 child = PackageTreeNode(path, self.relative + entry + "/", name=entry)
158                                 if not child.type.isModLike():
159                                         raise MinetestCheckError(("Expecting mod or modpack, found {} at {} inside {}") \
160                                                         .format(child.type.value, child.relative, self.type.value))
161
162                                 if child.name is None:
163                                         raise MinetestCheckError(("Missing base name for mod at {}").format(self.relative))
164
165                                 self.children.append(child)
166
167         def getModNames(self):
168                 return self.fold("name", type=ContentType.MOD)
169
170         # attr: Attribute name
171         # key: Key in attribute
172         # retval: Accumulator
173         # type: Filter to type
174         def fold(self, attr, key=None, retval=None, type=None):
175                 if retval is None:
176                         retval = set()
177
178                 # Iterate through children
179                 for child in self.children:
180                         child.fold(attr, key, retval, type)
181
182                 # Filter on type
183                 if type and type != self.type:
184                         return retval
185
186                 # Get attribute
187                 at = getattr(self, attr)
188                 if not at:
189                         return retval
190
191                 # Get value
192                 value = at if key is None else at.get(key)
193                 if isinstance(value, list):
194                         retval |= set(value)
195                 elif value:
196                         retval.add(value)
197
198                 return retval
199
200         def get(self, key):
201                 return self.meta.get(key)
202
203         def validate(self):
204                 for child in self.children:
205                         child.validate()