1# -*- coding: utf-8 -*- 2# 3# Picard, the next-generation MusicBrainz tagger 4# 5# Copyright (C) 2006-2009, 2012 Lukáš Lalinský 6# Copyright (C) 2007 Javier Kohen 7# Copyright (C) 2008-2011, 2014-2015, 2018-2020 Philipp Wolfer 8# Copyright (C) 2009 Carlin Mangar 9# Copyright (C) 2009 Nikolai Prokoschenko 10# Copyright (C) 2011-2012 Michael Wiencek 11# Copyright (C) 2012 Chad Wilson 12# Copyright (C) 2012 stephen 13# Copyright (C) 2012, 2014, 2017 Wieland Hoffmann 14# Copyright (C) 2013-2014, 2017-2020 Laurent Monin 15# Copyright (C) 2014, 2017 Sophist-UK 16# Copyright (C) 2016-2017 Sambhav Kothari 17# Copyright (C) 2016-2017 Ville Skyttä 18# Copyright (C) 2017-2018 Antonio Larrosa 19# Copyright (C) 2018 Calvin Walton 20# Copyright (C) 2018 virusMac 21# Copyright (C) 2020 Bob Swift 22# 23# This program is free software; you can redistribute it and/or 24# modify it under the terms of the GNU General Public License 25# as published by the Free Software Foundation; either version 2 26# of the License, or (at your option) any later version. 27# 28# This program is distributed in the hope that it will be useful, 29# but WITHOUT ANY WARRANTY; without even the implied warranty of 30# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31# GNU General Public License for more details. 32# 33# You should have received a copy of the GNU General Public License 34# along with this program; if not, write to the Free Software 35# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 36 37from collections.abc import MutableSequence 38from queue import LifoQueue 39 40from picard.metadata import ( 41 MULTI_VALUED_JOINER, 42 Metadata, 43) 44from picard.plugin import ExtensionPoint 45 46 47class ScriptError(Exception): 48 pass 49 50 51class ScriptParseError(ScriptError): 52 def __init__(self, stackitem, message): 53 super().__init__( 54 "{prefix:s}: {message:s}".format( 55 prefix=str(stackitem), 56 message=message 57 ) 58 ) 59 60 61class ScriptEndOfFile(ScriptParseError): 62 def __init__(self, stackitem): 63 super().__init__( 64 stackitem, 65 "Unexpected end of script" 66 ) 67 68 69class ScriptSyntaxError(ScriptParseError): 70 pass 71 72 73class ScriptUnknownFunction(ScriptParseError): 74 def __init__(self, stackitem): 75 super().__init__( 76 stackitem, 77 "Unknown function '{name}'".format(name=stackitem.name) 78 ) 79 80 81class ScriptRuntimeError(ScriptError): 82 def __init__(self, stackitem, message='Unknown error'): 83 super().__init__( 84 "{prefix:s}: {message:s}".format( 85 prefix=str(stackitem), 86 message=message 87 ) 88 ) 89 90 91class StackItem: 92 def __init__(self, line, column, name=None): 93 self.line = line 94 self.column = column 95 if name is None: 96 self.name = None 97 else: 98 self.name = '$' + name 99 100 def __str__(self): 101 if self.name is None: 102 return '{line:d}:{column:d}'.format( 103 line=self.line, 104 column=self.column 105 ) 106 else: 107 return '{line:d}:{column:d}:{name}'.format( 108 line=self.line, 109 column=self.column, 110 name=self.name 111 ) 112 113 114class ScriptText(str): 115 116 def eval(self, state): 117 return self 118 119 120def normalize_tagname(name): 121 if name.startswith('_'): 122 return "~" + name[1:] 123 return name 124 125 126class ScriptVariable(object): 127 128 def __init__(self, name): 129 self.name = name 130 131 def __repr__(self): 132 return '<ScriptVariable %%%s%%>' % self.name 133 134 def eval(self, state): 135 return state.context.get(normalize_tagname(self.name), "") 136 137 138class ScriptFunction(object): 139 140 def __init__(self, name, args, parser, column=0, line=0): 141 self.stackitem = StackItem(line, column, name) 142 try: 143 argnum_bound = parser.functions[name].argcount 144 argcount = len(args) 145 if argnum_bound: 146 too_few_args = argcount < argnum_bound.lower 147 if argnum_bound.upper is not None: 148 if argnum_bound.lower == argnum_bound.upper: 149 expected = "exactly %i" % argnum_bound.lower 150 else: 151 expected = "between %i and %i" % (argnum_bound.lower, argnum_bound.upper) 152 too_many_args = argcount > argnum_bound.upper 153 else: 154 expected = "at least %i" % argnum_bound.lower 155 too_many_args = False 156 157 if too_few_args or too_many_args: 158 raise ScriptSyntaxError( 159 self.stackitem, 160 "Wrong number of arguments for $%s: Expected %s, got %i" 161 % (name, expected, argcount) 162 ) 163 except KeyError: 164 raise ScriptUnknownFunction(self.stackitem) 165 166 self.name = name 167 self.args = args 168 169 def __repr__(self): 170 return "<ScriptFunction $%s(%r)>" % (self.name, self.args) 171 172 def eval(self, parser): 173 try: 174 function_registry_item = parser.functions[self.name] 175 except KeyError: 176 raise ScriptUnknownFunction(self.stackitem) 177 178 if function_registry_item.eval_args: 179 args = [arg.eval(parser) for arg in self.args] 180 else: 181 args = self.args 182 parser._function_stack.put(self.stackitem) 183 # Save return value to allow removing function from the stack on successful completion 184 return_value = function_registry_item.function(parser, *args) 185 parser._function_stack.get() 186 return return_value 187 188 189class ScriptExpression(list): 190 191 def eval(self, state): 192 return "".join([item.eval(state) for item in self]) 193 194 195def isidentif(ch): 196 return ch.isalnum() or ch == '_' 197 198 199class ScriptParser(object): 200 201 r"""Tagger script parser. 202 203Grammar: 204 text ::= [^$%] | '\$' | '\%' | '\(' | '\)' | '\,' 205 argtext ::= [^$%(),] | '\$' | '\%' | '\(' | '\)' | '\,' 206 identifier ::= [a-zA-Z0-9_] 207 variable ::= '%' identifier '%' 208 function ::= '$' identifier '(' (argument (',' argument)*)? ')' 209 expression ::= (variable | function | text)* 210 argument ::= (variable | function | argtext)* 211""" 212 213 _function_registry = ExtensionPoint(label='function_registry') 214 _cache = {} 215 216 def __init__(self): 217 self._function_stack = LifoQueue() 218 219 def __raise_eof(self): 220 raise ScriptEndOfFile(StackItem(line=self._y, column=self._x)) 221 222 def __raise_char(self, ch): 223 raise ScriptSyntaxError(StackItem(line=self._y, column=self._x), "Unexpected character '%s'" % ch) 224 225 def read(self): 226 try: 227 ch = self._text[self._pos] 228 except IndexError: 229 return None 230 else: 231 self._pos += 1 232 self._px = self._x 233 self._py = self._y 234 if ch == '\n': 235 self._line = self._pos 236 self._x = 1 237 self._y += 1 238 else: 239 self._x += 1 240 return ch 241 242 def unread(self): 243 self._pos -= 1 244 self._x = self._px 245 self._y = self._py 246 247 def parse_arguments(self): 248 results = [] 249 while True: 250 result, ch = self.parse_expression(False) 251 results.append(result) 252 if ch == ')': 253 # Only an empty expression as first argument 254 # is the same as no argument given. 255 if len(results) == 1 and results[0] == []: 256 return [] 257 return results 258 259 def parse_function(self): 260 start = self._pos 261 column = self._x - 2 # Set x position to start of function name ($) 262 line = self._y 263 while True: 264 ch = self.read() 265 if ch == '(': 266 name = self._text[start:self._pos-1] 267 if name not in self.functions: 268 raise ScriptUnknownFunction(StackItem(line, column, name)) 269 return ScriptFunction(name, self.parse_arguments(), self, column, line) 270 elif ch is None: 271 self.__raise_eof() 272 elif not isidentif(ch): 273 self.__raise_char(ch) 274 275 def parse_variable(self): 276 begin = self._pos 277 while True: 278 ch = self.read() 279 if ch == '%': 280 return ScriptVariable(self._text[begin:self._pos-1]) 281 elif ch is None: 282 self.__raise_eof() 283 elif not isidentif(ch) and ch != ':': 284 self.__raise_char(ch) 285 286 def parse_text(self, top): 287 text = [] 288 while True: 289 ch = self.read() 290 if ch == "\\": 291 ch = self.read() 292 if ch == 'n': 293 text.append('\n') 294 elif ch == 't': 295 text.append('\t') 296 elif ch not in "$%(),\\": 297 self.__raise_char(ch) 298 else: 299 text.append(ch) 300 elif ch is None: 301 break 302 elif not top and ch == '(': 303 self.__raise_char(ch) 304 elif ch in '$%' or (not top and ch in ',)'): 305 self.unread() 306 break 307 else: 308 text.append(ch) 309 return ScriptText("".join(text)) 310 311 def parse_expression(self, top): 312 tokens = ScriptExpression() 313 while True: 314 ch = self.read() 315 if ch is None: 316 if top: 317 break 318 else: 319 self.__raise_eof() 320 elif not top and ch in ',)': 321 break 322 elif ch == '$': 323 tokens.append(self.parse_function()) 324 elif ch == '%': 325 tokens.append(self.parse_variable()) 326 else: 327 self.unread() 328 tokens.append(self.parse_text(top)) 329 return (tokens, ch) 330 331 def load_functions(self): 332 self.functions = {} 333 for name, item in ScriptParser._function_registry: 334 self.functions[name] = item 335 336 def parse(self, script, functions=False): 337 """Parse the script.""" 338 self._text = script 339 self._pos = 0 340 self._px = self._x = 1 341 self._py = self._y = 1 342 self._line = 0 343 if not functions: 344 self.load_functions() 345 return self.parse_expression(True)[0] 346 347 def eval(self, script, context=None, file=None): 348 """Parse and evaluate the script.""" 349 self.context = context if context is not None else Metadata() 350 self.file = file 351 self.load_functions() 352 key = hash(script) 353 if key not in ScriptParser._cache: 354 ScriptParser._cache[key] = self.parse(script, True) 355 return ScriptParser._cache[key].eval(self) 356 357 358class MultiValue(MutableSequence): 359 def __init__(self, parser, multi, separator): 360 self.parser = parser 361 if isinstance(separator, ScriptExpression): 362 self.separator = separator.eval(self.parser) 363 else: 364 self.separator = separator 365 if (self.separator == MULTI_VALUED_JOINER 366 and len(multi) == 1 367 and isinstance(multi[0], ScriptVariable)): 368 # Convert ScriptExpression containing only a single variable into variable 369 self._multi = self.parser.context.getall(normalize_tagname(multi[0].name)) 370 else: 371 # Fall-back to converting to a string and splitting if haystack is an expression 372 # or user has overridden the separator character. 373 evaluated_multi = multi.eval(self.parser) 374 if not evaluated_multi: 375 self._multi = [] 376 elif self.separator: 377 self._multi = evaluated_multi.split(self.separator) 378 else: 379 self._multi = [evaluated_multi] 380 381 def __len__(self): 382 return len(self._multi) 383 384 def __getitem__(self, key): 385 return self._multi[key] 386 387 def __setitem__(self, key, value): 388 self._multi[key] = value 389 390 def __delitem__(self, key): 391 del self._multi[key] 392 393 def insert(self, index, value): 394 return self._multi.insert(index, value) 395 396 def __repr__(self): 397 return '%s(%r, %r, %r)' % (self.__class__.__name__, self.parser, self._multi, self.separator) 398 399 def __str__(self): 400 return self.separator.join([x for x in self if x]) 401