]> git.lizzy.rs Git - cheatdb.git/blob - app/tasks/minetestcheck/tree.py
07e67f68261600d96beafca9dbefedfc57016e47
[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                 # Check dependencies
132                 for dep in result["depends"]:
133                         if not basenamePattern.match(dep):
134                                 raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, names must only contain a-z0-9_.") \
135                                         .format(dep, self.relative))
136
137                 for dep in result["optional_depends"]:
138                         if not basenamePattern.match(dep):
139                                 raise MinetestCheckError(("Invalid dependency name '{}' for mod at {}, names must only contain a-z0-9_.") \
140                                         .format(dep, self.relative))
141
142
143                 # Fix games using "name" as "title"
144                 if self.type == ContentType.GAME:
145                         result["title"] = result["name"]
146                         del result["name"]
147
148                 # Calculate Title
149                 if "name" in result and not "title" in result:
150                         result["title"] = result["name"].replace("_", " ").title()
151
152                 # Calculate short description
153                 if "description" in result:
154                         desc = result["description"]
155                         idx = desc.find(".") + 1
156                         cutIdx = min(len(desc), 200 if idx < 5 else idx)
157                         result["short_description"] = desc[:cutIdx]
158
159                 if "name" in result:
160                         self.name = result["name"]
161                         del result["name"]
162
163                 self.meta = result
164
165         def add_children_from_mod_dir(self, subdir):
166                 dir = self.baseDir
167                 relative = self.relative
168                 if subdir:
169                         dir += "/" + subdir
170                         relative += subdir + "/"
171
172                 for entry in next(os.walk(dir))[1]:
173                         path = os.path.join(dir, entry)
174                         if not entry.startswith('.') and os.path.isdir(path):
175                                 child = PackageTreeNode(path, relative + entry + "/", name=entry)
176                                 if not child.type.isModLike():
177                                         raise MinetestCheckError(("Expecting mod or modpack, found {} at {} inside {}") \
178                                                         .format(child.type.value, child.relative, self.type.value))
179
180                                 if child.name is None:
181                                         raise MinetestCheckError(("Missing base name for mod at {}").format(self.relative))
182
183                                 self.children.append(child)
184
185         def getModNames(self):
186                 return self.fold("name", type=ContentType.MOD)
187
188         # attr: Attribute name
189         # key: Key in attribute
190         # retval: Accumulator
191         # type: Filter to type
192         def fold(self, attr, key=None, retval=None, type=None):
193                 if retval is None:
194                         retval = set()
195
196                 # Iterate through children
197                 for child in self.children:
198                         child.fold(attr, key, retval, type)
199
200                 # Filter on type
201                 if type and type != self.type:
202                         return retval
203
204                 # Get attribute
205                 at = getattr(self, attr)
206                 if not at:
207                         return retval
208
209                 # Get value
210                 value = at if key is None else at.get(key)
211                 if isinstance(value, list):
212                         retval |= set(value)
213                 elif value:
214                         retval.add(value)
215
216                 return retval
217
218         def get(self, key):
219                 return self.meta.get(key)
220
221         def validate(self):
222                 for child in self.children:
223                         child.validate()