xref: /aosp_15_r20/external/angle/third_party/logdog/logdog/streamname.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1# Copyright 2016 The LUCI Authors. All rights reserved.
2# Use of this source code is governed under the Apache License, Version 2.0
3# that can be found in the LICENSE file.
4
5import collections
6import re
7import string
8
9# third_party/
10from six.moves import urllib
11
12_STREAM_SEP = '/'
13_ALNUM_CHARS = string.ascii_letters + string.digits
14_VALID_SEG_CHARS = _ALNUM_CHARS + ':_-.'
15_SEGMENT_RE_BASE = r'[a-zA-Z0-9][a-zA-Z0-9:_\-.]*'
16_SEGMENT_RE = re.compile('^' + _SEGMENT_RE_BASE + '$')
17_STREAM_NAME_RE = re.compile('^(' + _SEGMENT_RE_BASE + ')(/' + _SEGMENT_RE_BASE + ')*$')
18_MAX_STREAM_NAME_LENGTH = 4096
19
20_MAX_TAG_KEY_LENGTH = 64
21_MAX_TAG_VALUE_LENGTH = 4096
22
23
24def validate_stream_name(v, maxlen=None):
25    """Verifies that a given stream name is valid.
26
27  Args:
28    v (str): The stream name string.
29
30
31  Raises:
32    ValueError if the stream name is invalid.
33  """
34    maxlen = maxlen or _MAX_STREAM_NAME_LENGTH
35    if len(v) > maxlen:
36        raise ValueError('Maximum length exceeded (%d > %d)' % (len(v), maxlen))
37    if _STREAM_NAME_RE.match(v) is None:
38        raise ValueError('Invalid stream name: %r' % v)
39
40
41def validate_tag(key, value):
42    """Verifies that a given tag key/value is valid.
43
44  Args:
45    k (str): The tag key.
46    v (str): The tag value.
47
48  Raises:
49    ValueError if the tag is not valid.
50  """
51    validate_stream_name(key, maxlen=_MAX_TAG_KEY_LENGTH)
52    validate_stream_name(value, maxlen=_MAX_TAG_VALUE_LENGTH)
53
54
55def normalize_segment(seg, prefix=None):
56    """Given a string, mutate it into a valid segment name.
57
58  This operates by replacing invalid segment name characters with underscores
59  (_) when encountered.
60
61  A special case is when "seg" begins with non-alphanumeric character. In this
62  case, we will prefix it with the "prefix", if one is supplied. Otherwise,
63  raises ValueError.
64
65  See _VALID_SEG_CHARS for all valid characters for a segment.
66
67  Raises:
68    ValueError: If normalization could not be successfully performed.
69  """
70    if not seg:
71        if prefix is None:
72            raise ValueError('Cannot normalize empty segment with no prefix.')
73        seg = prefix
74    else:
75
76        def replace_if_invalid(ch, first=False):
77            ret = ch if ch in _VALID_SEG_CHARS else '_'
78            if first and ch not in _ALNUM_CHARS:
79                if prefix is None:
80                    raise ValueError('Segment has invalid beginning, and no prefix was '
81                                     'provided.')
82                return prefix + ret
83            return ret
84
85        seg = ''.join(replace_if_invalid(ch, i == 0) for i, ch in enumerate(seg))
86
87    if _SEGMENT_RE.match(seg) is None:
88        raise AssertionError('Normalized segment is still invalid: %r' % seg)
89
90    return seg
91
92
93def normalize(v, prefix=None):
94    """Given a string, mutate it into a valid stream name.
95
96  This operates by replacing invalid stream name characters with underscores (_)
97  when encountered.
98
99  A special case is when any segment of "v" begins with an non-alphanumeric
100  character. In this case, we will prefix the segment with the "prefix", if one
101  is supplied. Otherwise, raises ValueError.
102
103  See _STREAM_NAME_RE for a description of a valid stream name.
104
105  Raises:
106    ValueError: If normalization could not be successfully performed.
107  """
108    normalized = _STREAM_SEP.join(
109        normalize_segment(seg, prefix=prefix) for seg in v.split(_STREAM_SEP))
110    # Validate the resulting string.
111    validate_stream_name(normalized)
112    return normalized
113
114
115class StreamPath(collections.namedtuple('_StreamPath', ('prefix', 'name'))):
116    """StreamPath is a full stream path.
117
118  This consists of both a stream prefix and a stream name.
119
120  When constructed with parse or make, the stream path must be completely valid.
121  However, invalid stream paths may be constructed by manually instantiation.
122  This can be useful for wildcard query values (e.g., "prefix='foo/*/bar/**'").
123  """
124
125    @classmethod
126    def make(cls, prefix, name):
127        """Returns (StreamPath): The validated StreamPath instance.
128
129    Args:
130      prefix (str): the prefix component
131      name (str): the name component
132
133    Raises:
134      ValueError: If path is not a full, valid stream path string.
135    """
136        inst = cls(prefix=prefix, name=name)
137        inst.validate()
138        return inst
139
140    @classmethod
141    def parse(cls, path):
142        """Returns (StreamPath): The parsed StreamPath instance.
143
144    Args:
145      path (str): the full stream path to parse.
146
147    Raises:
148      ValueError: If path is not a full, valid stream path string.
149    """
150        parts = path.split('/+/', 1)
151        if len(parts) != 2:
152            raise ValueError('Not a full stream path: [%s]' % (path,))
153        return cls.make(*parts)
154
155    def validate(self):
156        """Raises: ValueError if this is not a valid stream name."""
157        try:
158            validate_stream_name(self.prefix)
159        except ValueError as e:
160            raise ValueError('Invalid prefix component [%s]: %s' % (
161                self.prefix,
162                e,
163            ))
164
165        try:
166            validate_stream_name(self.name)
167        except ValueError as e:
168            raise ValueError('Invalid name component [%s]: %s' % (
169                self.name,
170                e,
171            ))
172
173    def __str__(self):
174        return '%s/+/%s' % (self.prefix, self.name)
175
176
177def get_logdog_viewer_url(host, project, *stream_paths):
178    """Returns (str): The LogDog viewer URL for the named stream(s).
179
180  Args:
181    host (str): The name of the Coordiantor host.
182    project (str): The project name.
183    stream_paths: A set of StreamPath instances for the stream paths to
184        generate the URL for.
185  """
186    return urllib.parse.urlunparse((
187        'https',  # Scheme
188        host,  # netloc
189        'v/',  # path
190        '',  # params
191        '&'.join(('s=%s' % (urllib.parse.quote('%s/%s' % (project, path), ''))
192                  for path in stream_paths)),  # query
193        '',  # fragment
194    ))
195