1// Copyright 2016 The Prometheus Authors
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6//     http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package opentsdb
15
16import (
17	"bytes"
18	"fmt"
19
20	"github.com/prometheus/common/model"
21)
22
23// TagValue is a model.LabelValue that implements json.Marshaler and
24// json.Unmarshaler. These implementations avoid characters illegal in
25// OpenTSDB. See the MarshalJSON for details. TagValue is used for the values of
26// OpenTSDB tags as well as for OpenTSDB metric names.
27type TagValue model.LabelValue
28
29// MarshalJSON marshals this TagValue into JSON that only contains runes allowed
30// in OpenTSDB. It implements json.Marshaler. The runes allowed in OpenTSDB are
31// all single-byte. This function encodes the arbitrary byte sequence found in
32// this TagValue in the following way:
33//
34// - The string that underlies TagValue is scanned byte by byte.
35//
36// - If a byte represents a legal OpenTSDB rune with the exception of '_', that
37// byte is directly copied to the resulting JSON byte slice.
38//
39// - If '_' is encountered, it is replaced by '__'.
40//
41// - If ':' is encountered, it is replaced by '_.'.
42//
43// - All other bytes are replaced by '_' followed by two bytes containing the
44// uppercase ASCII representation of their hexadecimal value.
45//
46// This encoding allows to save arbitrary Go strings in OpenTSDB. That's
47// required because Prometheus label values can contain anything, and even
48// Prometheus metric names may (and often do) contain ':' (which is disallowed
49// in OpenTSDB strings). The encoding uses '_' as an escape character and
50// renders a ':' more or less recognizable as '_.'
51//
52// Examples:
53//
54// "foo-bar-42" -> "foo-bar-42"
55//
56// "foo_bar_42" -> "foo__bar__42"
57//
58// "http://example.org:8080" -> "http_.//example.org_.8080"
59//
60// "Björn's email: bjoern@soundcloud.com" ->
61// "Bj_C3_B6rn_27s_20email_._20bjoern_40soundcloud.com"
62//
63// "日" -> "_E6_97_A5"
64func (tv TagValue) MarshalJSON() ([]byte, error) {
65	length := len(tv)
66	// Need at least two more bytes than in tv.
67	result := bytes.NewBuffer(make([]byte, 0, length+2))
68	result.WriteByte('"')
69	for i := 0; i < length; i++ {
70		b := tv[i]
71		switch {
72		case (b >= '-' && b <= '9') || // '-', '.', '/', 0-9
73			(b >= 'A' && b <= 'Z') ||
74			(b >= 'a' && b <= 'z'):
75			result.WriteByte(b)
76		case b == '_':
77			result.WriteString("__")
78		case b == ':':
79			result.WriteString("_.")
80		default:
81			result.WriteString(fmt.Sprintf("_%X", b))
82		}
83	}
84	result.WriteByte('"')
85	return result.Bytes(), nil
86}
87
88// UnmarshalJSON unmarshals JSON strings coming from OpenTSDB into Go strings
89// by applying the inverse of what is described for the MarshalJSON method.
90func (tv *TagValue) UnmarshalJSON(json []byte) error {
91	escapeLevel := 0 // How many bytes after '_'.
92	var parsedByte byte
93
94	// Might need fewer bytes, but let's avoid realloc.
95	result := bytes.NewBuffer(make([]byte, 0, len(json)-2))
96
97	for i, b := range json {
98		if i == 0 {
99			if b != '"' {
100				return fmt.Errorf("expected '\"', got %q", b)
101			}
102			continue
103		}
104		if i == len(json)-1 {
105			if b != '"' {
106				return fmt.Errorf("expected '\"', got %q", b)
107			}
108			break
109		}
110		switch escapeLevel {
111		case 0:
112			if b == '_' {
113				escapeLevel = 1
114				continue
115			}
116			result.WriteByte(b)
117		case 1:
118			switch {
119			case b == '_':
120				result.WriteByte('_')
121				escapeLevel = 0
122			case b == '.':
123				result.WriteByte(':')
124				escapeLevel = 0
125			case b >= '0' && b <= '9':
126				parsedByte = (b - 48) << 4
127				escapeLevel = 2
128			case b >= 'A' && b <= 'F': // A-F
129				parsedByte = (b - 55) << 4
130				escapeLevel = 2
131			default:
132				return fmt.Errorf(
133					"illegal escape sequence at byte %d (%c)",
134					i, b,
135				)
136			}
137		case 2:
138			switch {
139			case b >= '0' && b <= '9':
140				parsedByte += b - 48
141			case b >= 'A' && b <= 'F': // A-F
142				parsedByte += b - 55
143			default:
144				return fmt.Errorf(
145					"illegal escape sequence at byte %d (%c)",
146					i, b,
147				)
148			}
149			result.WriteByte(parsedByte)
150			escapeLevel = 0
151		default:
152			panic("unexpected escape level")
153		}
154	}
155	*tv = TagValue(result.String())
156	return nil
157}
158