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