# coding=utf-8 # # Copyright 2011-2015 Splunk, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"): you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import absolute_import, division, print_function, unicode_literals from json.encoder import encode_basestring_ascii as json_encode_string from collections import namedtuple from ..six.moves import StringIO from io import open import csv import os import re from .. import six from ..six.moves import getcwd class Validator(object): """ Base class for validators that check and format search command options. You must inherit from this class and override :code:`Validator.__call__` and :code:`Validator.format`. :code:`Validator.__call__` should convert the value it receives as argument and then return it or raise a :code:`ValueError`, if the value will not convert. :code:`Validator.format` should return a human readable version of the value it receives as argument the same way :code:`str` does. """ def __call__(self, value): raise NotImplementedError() def format(self, value): raise NotImplementedError() class Boolean(Validator): """ Validates Boolean option values. """ truth_values = { '1': True, '0': False, 't': True, 'f': False, 'true': True, 'false': False, 'y': True, 'n': False, 'yes': True, 'no': False } def __call__(self, value): if not (value is None or isinstance(value, bool)): value = six.text_type(value).lower() if value not in Boolean.truth_values: raise ValueError('Unrecognized truth value: {0}'.format(value)) value = Boolean.truth_values[value] return value def format(self, value): return None if value is None else 't' if value else 'f' class Code(Validator): """ Validates code option values. This validator compiles an option value into a Python code object that can be executed by :func:`exec` or evaluated by :func:`eval`. The value returned is a :func:`namedtuple` with two members: object, the result of compilation, and source, the original option value. """ def __init__(self, mode='eval'): """ :param mode: Specifies what kind of code must be compiled; it can be :const:`'exec'`, if source consists of a sequence of statements, :const:`'eval'`, if it consists of a single expression, or :const:`'single'` if it consists of a single interactive statement. In the latter case, expression statements that evaluate to something other than :const:`None` will be printed. :type mode: unicode or bytes """ self._mode = mode def __call__(self, value): if value is None: return None try: return Code.object(compile(value, 'string', self._mode), six.text_type(value)) except (SyntaxError, TypeError) as error: raise ValueError(error.message) def format(self, value): return None if value is None else value.source object = namedtuple('Code', ('object', 'source')) class Fieldname(Validator): """ Validates field name option values. """ pattern = re.compile(r'''[_.a-zA-Z-][_.a-zA-Z0-9-]*$''') def __call__(self, value): if value is not None: value = six.text_type(value) if Fieldname.pattern.match(value) is None: raise ValueError('Illegal characters in fieldname: {}'.format(value)) return value def format(self, value): return value class File(Validator): """ Validates file option values. """ def __init__(self, mode='rt', buffering=None, directory=None): self.mode = mode self.buffering = buffering self.directory = File._var_run_splunk if directory is None else directory def __call__(self, value): if value is None: return value path = six.text_type(value) if not os.path.isabs(path): path = os.path.join(self.directory, path) try: value = open(path, self.mode) if self.buffering is None else open(path, self.mode, self.buffering) except IOError as error: raise ValueError('Cannot open {0} with mode={1} and buffering={2}: {3}'.format( value, self.mode, self.buffering, error)) return value def format(self, value): return None if value is None else value.name _var_run_splunk = os.path.join( os.environ['SPLUNK_HOME'] if 'SPLUNK_HOME' in os.environ else getcwd(), 'var', 'run', 'splunk') class Integer(Validator): """ Validates integer option values. """ def __init__(self, minimum=None, maximum=None): if minimum is not None and maximum is not None: def check_range(value): if not (minimum <= value <= maximum): raise ValueError('Expected integer in the range [{0},{1}], not {2}'.format(minimum, maximum, value)) return elif minimum is not None: def check_range(value): if value < minimum: raise ValueError('Expected integer in the range [{0},+∞], not {1}'.format(minimum, value)) return elif maximum is not None: def check_range(value): if value > maximum: raise ValueError('Expected integer in the range [-∞,{0}], not {1}'.format(maximum, value)) return else: def check_range(value): return self.check_range = check_range return def __call__(self, value): if value is None: return None try: if six.PY2: value = long(value) else: value = int(value) except ValueError: raise ValueError('Expected integer value, not {}'.format(json_encode_string(value))) self.check_range(value) return value def format(self, value): return None if value is None else six.text_type(int(value)) class Duration(Validator): """ Validates duration option values. """ def __call__(self, value): if value is None: return None p = value.split(':', 2) result = None _60 = Duration._60 _unsigned = Duration._unsigned try: if len(p) == 1: result = _unsigned(p[0]) if len(p) == 2: result = 60 * _unsigned(p[0]) + _60(p[1]) if len(p) == 3: result = 3600 * _unsigned(p[0]) + 60 * _60(p[1]) + _60(p[2]) except ValueError: raise ValueError('Invalid duration value: {0}'.format(value)) return result def format(self, value): if value is None: return None value = int(value) s = value % 60 m = value // 60 % 60 h = value // (60 * 60) return '{0:02d}:{1:02d}:{2:02d}'.format(h, m, s) _60 = Integer(0, 59) _unsigned = Integer(0) class List(Validator): """ Validates a list of strings """ class Dialect(csv.Dialect): """ Describes the properties of list option values. """ strict = True delimiter = b',' quotechar = b'"' doublequote = True lineterminator = b'\n' skipinitialspace = True quoting = csv.QUOTE_MINIMAL def __init__(self, validator=None): if not (validator is None or isinstance(validator, Validator)): raise ValueError('Expected a Validator instance or None for validator, not {}', repr(validator)) self._validator = validator def __call__(self, value): if value is None or isinstance(value, list): return value try: value = next(csv.reader([value], self.Dialect)) except csv.Error as error: raise ValueError(error) if self._validator is None: return value try: for index, item in enumerate(value): value[index] = self._validator(item) except ValueError as error: raise ValueError('Could not convert item {}: {}'.format(index, error)) return value def format(self, value): output = StringIO() writer = csv.writer(output, List.Dialect) writer.writerow(value) value = output.getvalue() return value[:-1] class Map(Validator): """ Validates map option values. """ def __init__(self, **kwargs): self.membership = kwargs def __call__(self, value): if value is None: return None value = six.text_type(value) if value not in self.membership: raise ValueError('Unrecognized value: {0}'.format(value)) return self.membership[value] def format(self, value): return None if value is None else list(self.membership.keys())[list(self.membership.values()).index(value)] class Match(Validator): """ Validates that a value matches a regular expression pattern. """ def __init__(self, name, pattern, flags=0): self.name = six.text_type(name) self.pattern = re.compile(pattern, flags) def __call__(self, value): if value is None: return None value = six.text_type(value) if self.pattern.match(value) is None: raise ValueError('Expected {}, not {}'.format(self.name, json_encode_string(value))) return value def format(self, value): return None if value is None else six.text_type(value) class OptionName(Validator): """ Validates option names. """ pattern = re.compile(r'''(?=\w)[^\d]\w*$''', re.UNICODE) def __call__(self, value): if value is not None: value = six.text_type(value) if OptionName.pattern.match(value) is None: raise ValueError('Illegal characters in option name: {}'.format(value)) return value def format(self, value): return None if value is None else six.text_type(value) class RegularExpression(Validator): """ Validates regular expression option values. """ def __call__(self, value): if value is None: return None try: value = re.compile(six.text_type(value)) except re.error as error: raise ValueError('{}: {}'.format(six.text_type(error).capitalize(), value)) return value def format(self, value): return None if value is None else value.pattern class Set(Validator): """ Validates set option values. """ def __init__(self, *args): self.membership = set(args) def __call__(self, value): if value is None: return None value = six.text_type(value) if value not in self.membership: raise ValueError('Unrecognized value: {}'.format(value)) return value def format(self, value): return self.__call__(value) __all__ = ['Boolean', 'Code', 'Duration', 'File', 'Integer', 'List', 'Map', 'RegularExpression', 'Set']