Skip to content

Commit bfcd4bb

Browse files
authored
Merge pull request from GHSA-x768-cvr2-345r
1 parent 0a71918 commit bfcd4bb

File tree

2 files changed

+211
-4
lines changed

2 files changed

+211
-4
lines changed

Sources/Prometheus/PrometheusCollectorRegistry.swift

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public final class PrometheusCollectorRegistry: Sendable {
7373
/// - Parameter name: A name to identify ``Counter``'s value.
7474
/// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry``
7575
public func makeCounter(name: String) -> Counter {
76-
self.box.withLockedValue { store -> Counter in
76+
let name = name.ensureValidMetricName()
77+
return self.box.withLockedValue { store -> Counter in
7778
guard let value = store[name] else {
7879
let counter = Counter(name: name, labels: [])
7980
store[name] = .counter(counter)
@@ -106,6 +107,9 @@ public final class PrometheusCollectorRegistry: Sendable {
106107
return self.makeCounter(name: name)
107108
}
108109

110+
let name = name.ensureValidMetricName()
111+
let labels = labels.ensureValidLabelNames()
112+
109113
return self.box.withLockedValue { store -> Counter in
110114
guard let value = store[name] else {
111115
let labelNames = labels.allLabelNames
@@ -154,7 +158,8 @@ public final class PrometheusCollectorRegistry: Sendable {
154158
/// - Parameter name: A name to identify ``Gauge``'s value.
155159
/// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry``
156160
public func makeGauge(name: String) -> Gauge {
157-
self.box.withLockedValue { store -> Gauge in
161+
let name = name.ensureValidMetricName()
162+
return self.box.withLockedValue { store -> Gauge in
158163
guard let value = store[name] else {
159164
let gauge = Gauge(name: name, labels: [])
160165
store[name] = .gauge(gauge)
@@ -187,6 +192,9 @@ public final class PrometheusCollectorRegistry: Sendable {
187192
return self.makeGauge(name: name)
188193
}
189194

195+
let name = name.ensureValidMetricName()
196+
let labels = labels.ensureValidLabelNames()
197+
190198
return self.box.withLockedValue { store -> Gauge in
191199
guard let value = store[name] else {
192200
let labelNames = labels.allLabelNames
@@ -236,7 +244,8 @@ public final class PrometheusCollectorRegistry: Sendable {
236244
/// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram``
237245
/// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry``
238246
public func makeDurationHistogram(name: String, buckets: [Duration]) -> DurationHistogram {
239-
self.box.withLockedValue { store -> DurationHistogram in
247+
let name = name.ensureValidMetricName()
248+
return self.box.withLockedValue { store -> DurationHistogram in
240249
guard let value = store[name] else {
241250
let gauge = DurationHistogram(name: name, labels: [], buckets: buckets)
242251
store[name] = .durationHistogram(gauge)
@@ -274,6 +283,9 @@ public final class PrometheusCollectorRegistry: Sendable {
274283
return self.makeDurationHistogram(name: name, buckets: buckets)
275284
}
276285

286+
let name = name.ensureValidMetricName()
287+
let labels = labels.ensureValidLabelNames()
288+
277289
return self.box.withLockedValue { store -> DurationHistogram in
278290
guard let value = store[name] else {
279291
let labelNames = labels.allLabelNames
@@ -335,7 +347,8 @@ public final class PrometheusCollectorRegistry: Sendable {
335347
/// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram``
336348
/// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry``
337349
public func makeValueHistogram(name: String, buckets: [Double]) -> ValueHistogram {
338-
self.box.withLockedValue { store -> ValueHistogram in
350+
let name = name.ensureValidMetricName()
351+
return self.box.withLockedValue { store -> ValueHistogram in
339352
guard let value = store[name] else {
340353
let gauge = ValueHistogram(name: name, labels: [], buckets: buckets)
341354
store[name] = .valueHistogram(gauge)
@@ -364,6 +377,9 @@ public final class PrometheusCollectorRegistry: Sendable {
364377
return self.makeValueHistogram(name: name, buckets: buckets)
365378
}
366379

380+
let name = name.ensureValidMetricName()
381+
let labels = labels.ensureValidLabelNames()
382+
367383
return self.box.withLockedValue { store -> ValueHistogram in
368384
guard let value = store[name] else {
369385
let labelNames = labels.allLabelNames
@@ -560,6 +576,14 @@ extension [(String, String)] {
560576
result = result.sorted()
561577
return result
562578
}
579+
580+
fileprivate func ensureValidLabelNames() -> [(String, String)] {
581+
if self.allSatisfy({ $0.0.isValidLabelName() }) {
582+
return self
583+
} else {
584+
return self.map { ($0.ensureValidLabelName(), $1) }
585+
}
586+
}
563587
}
564588

565589
extension [UInt8] {
@@ -595,3 +619,91 @@ extension PrometheusMetric {
595619
return prerendered
596620
}
597621
}
622+
623+
extension String {
624+
fileprivate func isValidMetricName() -> Bool {
625+
var isFirstCharacter = true
626+
for ascii in self.utf8 {
627+
defer { isFirstCharacter = false }
628+
switch ascii {
629+
case UInt8(ascii: "A")...UInt8(ascii: "Z"),
630+
UInt8(ascii: "a")...UInt8(ascii: "z"),
631+
UInt8(ascii: "_"), UInt8(ascii: ":"):
632+
continue
633+
case UInt8(ascii: "0"), UInt8(ascii: "9"):
634+
if isFirstCharacter {
635+
return false
636+
}
637+
continue
638+
default:
639+
return false
640+
}
641+
}
642+
return true
643+
}
644+
645+
fileprivate func isValidLabelName() -> Bool {
646+
var isFirstCharacter = true
647+
for ascii in self.utf8 {
648+
defer { isFirstCharacter = false }
649+
switch ascii {
650+
case UInt8(ascii: "A")...UInt8(ascii: "Z"),
651+
UInt8(ascii: "a")...UInt8(ascii: "z"),
652+
UInt8(ascii: "_"):
653+
continue
654+
case UInt8(ascii: "0"), UInt8(ascii: "9"):
655+
if isFirstCharacter {
656+
return false
657+
}
658+
continue
659+
default:
660+
return false
661+
}
662+
}
663+
return true
664+
}
665+
666+
fileprivate func ensureValidMetricName() -> String {
667+
if self.isValidMetricName() {
668+
return self
669+
} else {
670+
var new = self
671+
new.fixPrometheusName(allowColon: true)
672+
return new
673+
}
674+
}
675+
676+
fileprivate func ensureValidLabelName() -> String {
677+
if self.isValidLabelName() {
678+
return self
679+
} else {
680+
var new = self
681+
new.fixPrometheusName(allowColon: false)
682+
return new
683+
}
684+
}
685+
686+
fileprivate mutating func fixPrometheusName(allowColon: Bool) {
687+
var startIndex = self.startIndex
688+
var isFirstCharacter = true
689+
while let fixIndex = self[startIndex...].firstIndex(where: { character in
690+
defer { isFirstCharacter = false }
691+
switch character {
692+
case "A"..."Z", "a"..."z", "_":
693+
return false
694+
case ":":
695+
return !allowColon
696+
case "0"..."9":
697+
return isFirstCharacter
698+
default:
699+
return true
700+
}
701+
}) {
702+
self.replaceSubrange(fixIndex...fixIndex, with: CollectionOfOne("_"))
703+
startIndex = fixIndex
704+
if startIndex == self.endIndex {
705+
break
706+
}
707+
}
708+
}
709+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftPrometheus open source project
4+
//
5+
// Copyright (c) 2024 SwiftPrometheus project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Prometheus
16+
import XCTest
17+
18+
final class ValidNamesTests: XCTestCase {
19+
func testCounterWithEmoji() {
20+
let client = PrometheusCollectorRegistry()
21+
let counter = client.makeCounter(name: "coffee☕️", labels: [])
22+
counter.increment()
23+
24+
var buffer = [UInt8]()
25+
client.emit(into: &buffer)
26+
XCTAssertEqual(
27+
String(decoding: buffer, as: Unicode.UTF8.self),
28+
"""
29+
# TYPE coffee_ counter
30+
coffee_ 1
31+
32+
"""
33+
)
34+
}
35+
36+
func testIllegalMetricNames() async throws {
37+
let registry = PrometheusCollectorRegistry()
38+
39+
/// Notably, newlines must not allow creating whole new metric root
40+
let tests = [
41+
"name",
42+
"""
43+
name{bad="haha"} 121212121
44+
bad_bad 12321323
45+
"""
46+
]
47+
48+
for test in tests {
49+
registry.makeCounter(
50+
name: test,
51+
labels: []
52+
).increment()
53+
}
54+
55+
var buffer = [UInt8]()
56+
registry.emit(into: &buffer)
57+
XCTAssertEqual(
58+
String(decoding: buffer, as: Unicode.UTF8.self).split(separator: "\n").sorted().joined(separator: "\n"),
59+
"""
60+
# TYPE name counter
61+
# TYPE name_bad__haha___121212121_bad_bad_12321323 counter
62+
name 1
63+
name_bad__haha___121212121_bad_bad_12321323 1
64+
"""
65+
)
66+
}
67+
68+
func testIllegalLabelNames() async throws {
69+
let registry = PrometheusCollectorRegistry()
70+
71+
let tests = [
72+
"""
73+
name{bad="haha"} 121212121
74+
bad_bad 12321323
75+
"""
76+
]
77+
78+
for test in tests {
79+
registry.makeCounter(
80+
name: "metric",
81+
labels: [(test, "value")]
82+
).increment()
83+
}
84+
85+
var buffer = [UInt8]()
86+
registry.emit(into: &buffer)
87+
XCTAssertEqual(
88+
String(decoding: buffer, as: Unicode.UTF8.self).split(separator: "\n").sorted().joined(separator: "\n"),
89+
"""
90+
# TYPE metric counter
91+
metric{name_bad__haha___121212121_bad_bad_12321323="value"} 1
92+
"""
93+
)
94+
}
95+
}

0 commit comments

Comments
 (0)