Repository: open-telemetry/opentelemetry-swift (swift)

Issue:
`W3CBaggagePropagator` Missing Percent Encoding for Special Characters in Values
## Issue Description
The current implementation of `W3CBaggagePropagator` in OpenTelemetry Swift does not properly handle percent encoding for special characters in baggage values, which is required by the [W3C Baggage Specification](https://www.w3.org/TR/baggage/#header-encoding).

## Current Behavior
The Swift implementation currently passes baggage values through directly without encoding:
```swift
headerContent += $0.key.name + "=" + $0.value.string
```

This means special characters in values are not properly encoded, which can lead to:
1. Interoperability issues with other OpenTelemetry implementations
2. Potential data corruption when values contain special characters
3. Non-compliance with the W3C Baggage specification

## Expected Behavior
According to the W3C Baggage specification:
1. Values MUST be percent-encoded if they contain special characters
2. The percent sign (`%`) itself MUST be percent-encoded
3. Characters outside the `baggage-octet` range MUST be percent-encoded
   - `baggage-octet` = `%x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E`

Example encodings:
Original: "DF 28" -> Encoded: "DF%2028"
Original: "Amélie" -> Encoded: "Am%C3%A9lie"
Original: "value%" -> Encoded: "value%25"


## Proposed Solution
1. Add percent encoding for values during injection:
```swift
let encodedValue = value.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? value
headerContent += key + "=" + encodedValue
```

2. Add percent decoding during extraction:
```swift
let decodedValue = String(keyValue[1]).removingPercentEncoding
```

3. Handle UTF-8 encoding properly:
   - Ensure all strings are properly UTF-8 encoded
   - Replace invalid UTF-8 sequences with the replacement character (U+FFFD) during decoding

## Impact
This issue affects interoperability with other OpenTelemetry implementations that correctly implement the W3C Baggage specification. Any application using the Swift SDK to propagate baggage with special characters may experience data loss or corruption.

## Related Information
- [W3C Baggage Specification - Header Encoding](https://www.w3.org/TR/baggage/#header-encoding)
- Similar implementations:
  - [Java implementation](https://github.com/open-telemetry/opentelemetry-java/blob/main/api/all/src/main/java/io/opentelemetry/api/baggage/propagation/W3CBaggagePropagator.java) uses `URLEncoder.encode()`
  - [Go implementation](https://github.com/open-telemetry/opentelemetry-go/blob/main/baggage/baggage.go) implements proper percent encoding

## Additional Context
This implementation should align with Swift's native string handling capabilities while maintaining compliance with the W3C specification.

Pull request:
## Description
This PR improves the W3C Baggage implementation to better align with the specification requirements, focusing on proper UTF-8 encoding and RFC7230 validation.

### Changes
- Implement proper UTF-8 percent encoding for baggage values
- Add RFC7230 token validation for baggage keys (using allowed characters: `!#$%&'*+-.^_`|~` and alphanumeric)
- Remove ASCII-only restriction from baggage values to allow any UTF-8 characters
- Maintain ASCII validation for resource attributes (separate concern from baggage)

### Key Implementation Details
* `EntryKey`: Now validates against RFC7230 token specification
* `EntryValue`: Allows any UTF-8 characters in values (will be percent-encoded during propagation)
* `W3CBaggagePropagator`: Implements proper percent encoding/decoding
* Tests: Added comprehensive W3C specification compliance tests

### Notes
- Removed previously proposed 180-byte length limit after reviewing other language implementations and considering potential data loss implications
- Maintains backward compatibility while improving spec compliance
- Added extensive test coverage for edge cases and W3C spec examples

### References
- Fixes #761
- [W3C Baggage Specification](https://www.w3.org/TR/baggage/#bib-rfc7230)

Patch:
diff --git a/Sources/OpenTelemetryApi/Baggage/EntryKey.swift b/Sources/OpenTelemetryApi/Baggage/EntryKey.swift
index db672af24..4781a832e 100644
--- a/Sources/OpenTelemetryApi/Baggage/EntryKey.swift
+++ b/Sources/OpenTelemetryApi/Baggage/EntryKey.swift
@@ -11,6 +11,14 @@ import Foundation
 /// EntryKeys are designed to be used as constants. Declaring each key as a constant
 /// prevents key names from being validated multiple times.
 public struct EntryKey: Equatable, Comparable, Hashable {
+  // RFC7230 token characters for valid keys
+  private static let validKeyCharacters: CharacterSet = {
+    var chars = CharacterSet()
+    // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
+    chars.insert(charactersIn: "!#$%&'*+-.^_`|~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
+    return chars
+  }()
+
   /// The maximum length for an entry key name. The value is 255.
   static let maxLength = 255
 
@@ -20,20 +28,27 @@ public struct EntryKey: Equatable, Comparable, Hashable {
   /// Constructs an EntryKey with the given name.
   /// The name must meet the following requirements:
   /// - It cannot be longer than maxLength.
-  /// - It can only contain printable ASCII characters.
+  /// - It can only contain RFC7230 token characters:
+  ///   - Letters (a-z, A-Z)
+  ///   - Numbers (0-9)
+  ///   - Special characters: ! # $ % & ' * + - . ^ _ ` | ~
+  /// - Leading and trailing whitespace is trimmed
   /// - Parameter name: the name of the key.
   public init?(name: String) {
-    let name = name.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
+    let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
     if !EntryKey.isValid(name: name) {
       return nil
     }
     self.name = name
   }
 
-  /// Determines whether the given String is a valid entry key.
-  /// - Parameter value: the entry key name to be validated.
   private static func isValid(name: String) -> Bool {
-    return name.count > 0 && name.count <= maxLength && StringUtils.isPrintableString(name)
+    guard name.count > 0 && name.count <= maxLength else {
+      return false
+    }
+
+    // Validate against RFC7230 token rules
+    return name.unicodeScalars.allSatisfy { validKeyCharacters.contains($0) }
   }
 
   public static func < (lhs: EntryKey, rhs: EntryKey) -> Bool {
diff --git a/Sources/OpenTelemetryApi/Baggage/EntryValue.swift b/Sources/OpenTelemetryApi/Baggage/EntryValue.swift
index 7da380435..f7573e7bb 100644
--- a/Sources/OpenTelemetryApi/Baggage/EntryValue.swift
+++ b/Sources/OpenTelemetryApi/Baggage/EntryValue.swift
@@ -30,6 +30,6 @@ public struct EntryValue: Equatable {
   /// Determines whether the given String is a valid entry value.
   /// - Parameter value: value the entry value to be validated.
   private static func isValid(value: String) -> Bool {
-    return value.count <= maxLength && StringUtils.isPrintableString(value)
+    return value.count <= maxLength
   }
 }
diff --git a/Sources/OpenTelemetryApi/Baggage/Propagation/W3CBaggagePropagator.swift b/Sources/OpenTelemetryApi/Baggage/Propagation/W3CBaggagePropagator.swift
index 53bbc9d63..ecab1fdc2 100644
--- a/Sources/OpenTelemetryApi/Baggage/Propagation/W3CBaggagePropagator.swift
+++ b/Sources/OpenTelemetryApi/Baggage/Propagation/W3CBaggagePropagator.swift
@@ -20,54 +20,68 @@ public struct W3CBaggagePropagator: TextMapBaggagePropagator {
 
   static let headerBaggage = "baggage"
 
+  private func isValidKeyValuePair(_ keyValue: String) -> (key: String, value: String)? {
+    let parts = keyValue.split(separator: "=", maxSplits: 1)
+    guard parts.count == 2 else { return nil }
+
+    return (String(parts[0]), String(parts[1]))
+  }
+
   public init() {}
 
   public let fields: Set<String> = [headerBaggage]
 
   public func inject(baggage: Baggage, carrier: inout [String: String], setter: some Setter) {
-    var headerContent = ""
-    baggage.getEntries().forEach {
-      headerContent += $0.key.name + "=" + $0.value.string
-      if let metadata = $0.metadata {
-        headerContent += ";" + metadata.metadata
+    var headerParts: [String] = []
+
+    for entry in baggage.getEntries() {
+      let key = entry.key.name.trimmingCharacters(in: .whitespaces)
+      guard !key.isEmpty else { continue }
+
+      // Use UTF-8 percent encoding for the value
+      let value = entry.value.string
+      let encodedValue = value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value
+
+      var part = "\(key)=\(encodedValue)"
+
+      if let metadata = entry.metadata?.metadata, !metadata.isEmpty {
+        part += ";\(metadata)"
       }
-      headerContent += ","
-    }
-    if !headerContent.isEmpty {
-      headerContent.removeLast()
+
+      headerParts.append(part)
     }
 
+    let headerContent = headerParts.joined(separator: ",")
     if !headerContent.isEmpty {
       setter.set(carrier: &carrier, key: W3CBaggagePropagator.headerBaggage, value: headerContent)
     }
   }
 
   public func extract(carrier: [String: String], getter: some Getter) -> Baggage? {
-    guard let baggageHeaderCollection = getter.get(carrier: carrier,
-                                                   key: W3CBaggagePropagator.headerBaggage),
-      let baggageHeader = baggageHeaderCollection.first else {
+    guard let baggageHeaderCollection = getter.get(carrier: carrier, key: W3CBaggagePropagator.headerBaggage),
+          let baggageHeader = baggageHeaderCollection.first else {
       return nil
     }
+
     let builder = OpenTelemetry.instance.baggageManager.baggageBuilder()
 
-    baggageHeader.split(separator: ",").forEach {
-      var entry = ""
-      var metadata = ""
-      if let separator = $0.firstIndex(of: ";") {
-        entry = String($0.prefix(upTo: separator))
-        metadata = String($0.suffix(from: separator).dropFirst())
-      } else {
-        entry = String($0)
-      }
+    let listMembers = baggageHeader.components(separatedBy: ",")
+    for listMember in listMembers {
+      let parts = listMember.split(separator: ";", maxSplits: 1)
+      guard !parts.isEmpty else { continue }
 
-      let keyValue = entry.split(separator: "=")
-      if keyValue.count != 2 {
-        return
-      }
-      if let key = EntryKey(name: String(keyValue[0])),
-         let value = EntryValue(string: String(keyValue[1])) {
-        builder.put(key: key, value: value, metadata: EntryMetadata(metadata: metadata))
+      // Validate and extract key-value pair
+      guard let (key, encodedValue) = isValidKeyValuePair(String(parts[0])),
+            let decodedValue = encodedValue.removingPercentEncoding,
+            let entryKey = EntryKey(name: key),
+            let entryValue = EntryValue(string: decodedValue) else {
+        continue
       }
+
+      let metadata = parts.count > 1 ? String(parts[1]) : nil
+      builder.put(key: entryKey,
+                  value: entryValue,
+                  metadata: EntryMetadata(metadata: metadata))
     }
 
     return builder.build()
diff --git a/Sources/OpenTelemetrySdk/Resources/Resource.swift b/Sources/OpenTelemetrySdk/Resources/Resource.swift
index 7b78946fa..b8f4a5888 100644
--- a/Sources/OpenTelemetrySdk/Resources/Resource.swift
+++ b/Sources/OpenTelemetrySdk/Resources/Resource.swift
@@ -69,6 +69,10 @@ public struct Resource: Equatable, Hashable, Codable {
 
   /// Determines whether the given String is a valid printable ASCII string with a length not
   /// exceed 255 characters.
+  /// According to OpenTelemetry specification, resource attribute keys:
+  /// - Must be non-null and non-empty string
+  /// - Must be printable ASCII characters (32-126)
+  /// - Maximum length 255 characters
   /// - Parameter name: the name to be validated.
   private static func isValid(name: String) -> Bool {
     return name.count <= maxLength && StringUtils.isPrintableString(name)


Test patch:
diff --git a/Tests/OpenTelemetryApiTests/Baggage/EntryKeyTests.swift b/Tests/OpenTelemetryApiTests/Baggage/EntryKeyTests.swift
index 35040d95f..41518494e 100644
--- a/Tests/OpenTelemetryApiTests/Baggage/EntryKeyTests.swift
+++ b/Tests/OpenTelemetryApiTests/Baggage/EntryKeyTests.swift
@@ -11,32 +11,58 @@ class EntryKeyTests: XCTestCase {
     XCTAssertEqual(EntryKey.maxLength, 255)
   }
 
-  func testGetName() {
-    XCTAssertEqual(EntryKey(name: "foo")?.name, "foo")
+  func testBasicKeyBehavior() {
+    // Basic creation and equality
+    let key1 = EntryKey(name: "foo")
+    let key2 = EntryKey(name: "foo")
+    let key3 = EntryKey(name: "bar")
+    
+    XCTAssertEqual(key1?.name, "foo")
+    XCTAssertEqual(key1, key2)
+    XCTAssertNotEqual(key1, key3)
   }
 
-  func testCreate_AllowEntryKeyNameWithMaxLength() {
-    let key = String(repeating: "k", count: EntryKey.maxLength)
-    XCTAssertEqual(EntryKey(name: key)?.name, key)
+  func testLengthValidation() {
+    // Test maximum length
+    let maxKey = String(repeating: "k", count: EntryKey.maxLength)
+    XCTAssertNotNil(EntryKey(name: maxKey))
+    
+    // Test over maximum length
+    let overMaxKey = String(repeating: "k", count: EntryKey.maxLength + 1)
+    XCTAssertNil(EntryKey(name: overMaxKey))
+    
+    // Test empty
+    XCTAssertNil(EntryKey(name: ""))
   }
 
-  func testCreate_DisallowEntryKeyNameOverMaxLength() {
-    let key = String(repeating: "k", count: EntryKey.maxLength + 1)
-    XCTAssertNil(EntryKey(name: key)?.name)
+  func testRFC7230TokenValidation() {
+    // Valid tokens according to RFC7230
+    XCTAssertNotNil(EntryKey(name: "simple-key"))
+    XCTAssertNotNil(EntryKey(name: "key_with_underscore"))
+    XCTAssertNotNil(EntryKey(name: "key.with.dots"))
+    XCTAssertNotNil(EntryKey(name: "!#$%&'*+-.^_`|~"))  // all special chars
+    XCTAssertNotNil(EntryKey(name: "123numeric456"))
+    
+    // Invalid tokens
+    XCTAssertNil(EntryKey(name: "key with spaces"))
+    XCTAssertNil(EntryKey(name: "key\twith\ttabs"))
+    XCTAssertNil(EntryKey(name: "key:with:colons"))
+    XCTAssertNil(EntryKey(name: "key/with/slashes"))
+    XCTAssertNil(EntryKey(name: "key@with@at"))
+    XCTAssertNil(EntryKey(name: "key[with]brackets"))
   }
 
-  func testCreate_DisallowUnprintableChars() {
-    let key = String("\0")
-    XCTAssertNil(EntryKey(name: key)?.name)
+  func testKeyTrimming() {
+    // Keys should be trimmed
+    XCTAssertEqual(EntryKey(name: "  key  ")?.name, "key")
+    XCTAssertEqual(EntryKey(name: "\tkey\t")?.name, "key")
+    XCTAssertEqual(EntryKey(name: "\nkey\n")?.name, "key")
   }
 
-  func testCreateString_DisallowEmpty() {
-    let key = String("")
-    XCTAssertNil(EntryKey(name: key)?.name)
-  }
-
-  func testEntryKeyEquals() {
-    XCTAssertEqual(EntryKey(name: "foo"), EntryKey(name: "foo"))
-    XCTAssertNotEqual(EntryKey(name: "foo"), EntryKey(name: "bar"))
+  func testCaseSensitivity() {
+    // Keys should be case-sensitive
+    let upperKey = EntryKey(name: "KEY")
+    let lowerKey = EntryKey(name: "key")
+    XCTAssertNotEqual(upperKey, lowerKey)
   }
 }
diff --git a/Tests/OpenTelemetryApiTests/Baggage/EntryValueTests.swift b/Tests/OpenTelemetryApiTests/Baggage/EntryValueTests.swift
index 46e96512c..ad59e7b25 100644
--- a/Tests/OpenTelemetryApiTests/Baggage/EntryValueTests.swift
+++ b/Tests/OpenTelemetryApiTests/Baggage/EntryValueTests.swift
@@ -25,9 +25,15 @@ class EntryValueTests: XCTestCase {
     XCTAssertNil(EntryValue(string: value)?.string)
   }
 
-  func testDisallowEntryValueWithUnprintableChars() {
-    let value = String("\0")
-    XCTAssertNil(EntryValue(string: value)?.string)
+  func testAllowAnyCharactersInValue() {
+    let value = String("\0")  // null character
+    XCTAssertNotNil(EntryValue(string: value))
+
+    let unicodeValue = "Amélie"  // unicode character
+    XCTAssertNotNil(EntryValue(string: unicodeValue))
+
+    let specialChars = "!@#$%^&*()"  // special characters
+    XCTAssertNotNil(EntryValue(string: specialChars))
   }
 
   func testEntryValueEquals() {
diff --git a/Tests/OpenTelemetryApiTests/Baggage/Propagation/W3CBaggagePropagatorTest.swift b/Tests/OpenTelemetryApiTests/Baggage/Propagation/W3CBaggagePropagatorTest.swift
index 27ac17cc3..1e4e2febd 100644
--- a/Tests/OpenTelemetryApiTests/Baggage/Propagation/W3CBaggagePropagatorTest.swift
+++ b/Tests/OpenTelemetryApiTests/Baggage/Propagation/W3CBaggagePropagatorTest.swift
@@ -16,73 +16,207 @@ class W3BaggagePropagatorTest: XCTestCase {
 
   override func tearDown() {}
 
-  func testFields() {
+  // MARK: - W3C Specification Compliance Tests
+
+  func testW3CFields() {
     XCTAssertEqual(propagator.fields.count, 1)
     XCTAssertTrue(propagator.fields.contains("baggage"))
   }
 
-  func testxtractNoBaggageHeader() {
+  func testW3CNoBaggageHeader() {
     let result = propagator.extract(carrier: [:], getter: getter)
     XCTAssertNil(result)
   }
 
-  func testExtractEmptyBaggageHeader() {
-    let result = propagator.extract(carrier: ["baggage": ""], getter: getter)
+  func testW3CEmptyBaggageHeader() {
+    let carrier = [
+      "baggage": ""
+    ]
+
+    let result = propagator.extract(carrier: carrier, getter: getter)
     XCTAssertEqual(result?.getEntries().count, 0)
   }
 
-  func testExtractSingleEntry() {
-    let result = propagator.extract(carrier: ["baggage": "key=value"], getter: getter)!
-    let expectedBaggage = builder.put(key: "key", value: "value").build()
+  func testW3CDuplicateKeys() {
+    let carrier = [
+      "baggage": "key=value1,key=value2"
+    ]
+
+    let result = propagator.extract(carrier: carrier, getter: getter)!
+    let expectedBaggage = builder.put(key: "key", value: "value2").build()
     XCTAssert(result == expectedBaggage)
   }
 
-  func testExtractMultiEntry() {
-    let result = propagator.extract(carrier: ["baggage": "key1=value1,key2=value2"], getter: getter)!
-    let expectedBaggage = builder.put(key: "key1", value: "value1").put(key: "key2", value: "value2").build()
-    XCTAssert(result == expectedBaggage)
+  func testW3CInjectEmptyBaggage() {
+    var carrier = [String: String]()
+    propagator.inject(baggage: EmptyBaggage.instance, carrier: &carrier, setter: setter)
+    XCTAssertEqual(carrier.count, 0)
   }
 
-  func testExtractDuplicateKeys() {
-    let result = propagator.extract(carrier: ["baggage": "key=value1,key=value2"], getter: getter)!
-    let expectedBaggage = builder.put(key: "key", value: "value2").build()
+  func testW3CExampleSingleHeader() {
+    // Test case from W3C spec: "userId=alice,serverNode=DF%2028,isProduction=false"
+    let carrier = [
+      "baggage": "userId=alice,serverNode=DF%2028,isProduction=false"
+    ]
+
+    let result = propagator.extract(carrier: carrier, getter: getter)!
+
+    let expectedBaggage = builder
+      .put(key: "userId", value: "alice")
+      .put(key: "serverNode", value: "DF 28")
+      .put(key: "isProduction", value: "false")
+      .build()
+
     XCTAssert(result == expectedBaggage)
   }
 
-  func testExtractWithMetadata() {
-    let result = propagator.extract(carrier: ["baggage": "key=value;metadata-key=value;othermetadata"], getter: getter)!
-    let expectedBaggage = builder.put(key: "key", value: "value", metadata: "metadata-key=value;othermetadata")
+  func testW3CExampleWithSpecialCharacters() {
+    // Test case from W3C spec: "userId=Am%C3%A9lie,serverNode=DF%2028,isProduction=false"
+    let carrier = [
+      "baggage": "userId=Am%C3%A9lie,serverNode=DF%2028,isProduction=false"
+    ]
+
+    let result = propagator.extract(carrier: carrier, getter: getter)!
+
+    let expectedBaggage = builder
+      .put(key: "userId", value: "Amélie")
+      .put(key: "serverNode", value: "DF 28")
+      .put(key: "isProduction", value: "false")
       .build()
+
     XCTAssert(result == expectedBaggage)
   }
 
-  func testExtractFullComplexities() {
-    let result = propagator.extract(carrier: ["baggage": "key1= value1; metadata-key = value; othermetadata, " +
-        "key2 =value2 , key3 =\tvalue3 ; "], getter: getter)!
-    let expectedBaggage = builder.put(key: "key1", value: "value1", metadata: "metadata-key = value; othermetadata")
-      .put(key: "key2", value: "value2")
-      .put(key: "key3", value: "value3")
+  func testW3CExampleMultipleHeaders() {
+    // Test case from W3C spec for multiple headers
+    let carrier = [
+      "baggage": "userId=alice",
+      "Baggage": "serverNode=DF%2028,isProduction=false"
+    ]
+
+    let result = propagator.extract(carrier: carrier, getter: getter)!
+
+    // According to spec, should only process the first header
+    let expectedBaggage = builder
+      .put(key: "userId", value: "alice")
       .build()
+
     XCTAssert(result == expectedBaggage)
   }
 
-  func testInjectEmptyBaggage() {
-    var carrier = [String: String]()
-    propagator.inject(baggage: EmptyBaggage.instance, carrier: &carrier, setter: setter)
-    XCTAssertEqual(carrier.count, 0)
+  func testW3CSpecExamples() {
+    // Examples directly from the spec
+    let examples = [
+      "key1=value1,key2=value2",
+      "key1 = value1, key2 = value2",
+      "key1=value1;property=value"
+    ]
+
+    for example in examples {
+      let result = propagator.extract(carrier: ["baggage": example], getter: getter)
+      XCTAssertNotNil(result, "Failed to parse valid example: \(example)")
+    }
   }
 
-  func testInject() {
-    let baggage = builder.put(key: "nometa", value: "nometa-value")
-      .put(key: "nometa", value: "nometa-value")
-      .put(key: "meta", value: "meta-value", metadata: "somemetadata; someother=foo")
+  func testW3CPropertyValues() {
+    // Test case with properties as defined in W3C spec
+    let result = propagator.extract(carrier: ["baggage": "key1=value1;property1;property2=value"], getter: getter)!
+
+    let expectedBaggage = builder
+      .put(key: "key1", value: "value1", metadata: "property1;property2=value")
       .build()
 
+    XCTAssert(result == expectedBaggage)
+  }
+
+  func testW3CInjectionExamples() {
     var carrier = [String: String]()
+
+    let baggage = builder
+      .put(key: "userId", value: "Amélie")
+      .put(key: "serverNode", value: "DF 28")
+      .put(key: "isProduction", value: "false")
+      .build()
+
     propagator.inject(baggage: baggage, carrier: &carrier, setter: setter)
 
-    let expected1 = ["baggage": "meta=meta-value;somemetadata; someother=foo,nometa=nometa-value"]
-    let expected2 = ["baggage": "nometa=nometa-value,meta=meta-value;somemetadata; someother=foo"]
-    XCTAssert(carrier == expected1 || carrier == expected2)
+    // Get the actual value for debugging
+    let actualValue = carrier["baggage"] ?? ""
+
+    // Split and sort the entries to compare content regardless of order
+    let actualEntries = Set(actualValue.split(separator: ",").map(String.init))
+    let expectedEntries = Set([
+      "userId=Am%C3%A9lie",
+      "serverNode=DF%2028",
+      "isProduction=false"
+    ])
+
+    XCTAssertEqual(actualEntries, expectedEntries,
+                   "Expected entries: \(expectedEntries)\nActual entries: \(actualEntries)")
+  }
+
+  func testW3CInvalidCharacters() {
+    let invalidInputs = [
+      // Invalid characters in key
+      "key@=value",      // @ not allowed in token
+      "key,=value",      // comma not allowed in token
+      "key;=value",      // semicolon not allowed in token
+      "key\"=value",     // quote not allowed in token
+
+      // Empty parts
+      "=value",          // empty key
+      "key=",            // empty value
+      "=",              // both empty
+      "",               // completely empty
+
+      // Invalid percent-encoding
+      "key=%",          // incomplete percent-encoding
+      "key=%XY",        // invalid hex digits
+    ]
+
+    for invalidInput in invalidInputs {
+      let carrier = ["baggage": invalidInput]
+      let baggage = propagator.extract(carrier: carrier, getter: getter)
+      XCTAssertEqual(baggage?.getEntries().count ?? 0, 0,
+                     "Should reject invalid input: \(invalidInput)")
+    }
+  }
+
+  func testW3CValidCharacters() {
+    let validInputs = [
+      // Basic valid case
+      "key=value",
+
+      // Whitespace cases
+      "key =value",
+      "key= value",
+      "key = value",
+      " key=value ",
+
+      // Valid key characters (RFC7230 token)
+      "key-1=value",
+      "key.2=value",
+      "KEY_3=value",
+
+      // Values with equals signs
+      "key=value=more",
+
+      // Percent-encoded values
+      "key=value%20with%20spaces",
+      "key=special%3Dequals",
+      "key=unicode%C3%A9",  // é
+
+      // Valid baggage-octets without encoding
+      "key=value-123",
+      "key=!value",
+      "key=value~"
+    ]
+
+    for validInput in validInputs {
+      let carrier = ["baggage": validInput]
+      let baggage = propagator.extract(carrier: carrier, getter: getter)
+      XCTAssertEqual(baggage?.getEntries().count, 1,
+                     "Should accept valid input: \(validInput)")
+    }
   }
 }
diff --git a/Tests/OpenTelemetryApiTests/Baggage/ScopedBaggageTests.swift b/Tests/OpenTelemetryApiTests/Baggage/ScopedBaggageTests.swift
index 0a06c5487..4a01b143b 100644
--- a/Tests/OpenTelemetryApiTests/Baggage/ScopedBaggageTests.swift
+++ b/Tests/OpenTelemetryApiTests/Baggage/ScopedBaggageTests.swift
@@ -8,9 +8,9 @@ import XCTest
 import OpenTelemetryTestUtils
 
 class ScopedBaggageTestsInfo: OpenTelemetryContextTestCase {
-  let key1 = EntryKey(name: "key 1")!
-  let key2 = EntryKey(name: "key 2")!
-  let key3 = EntryKey(name: "key 3")!
+  let key1 = EntryKey(name: "key-1")!
+  let key2 = EntryKey(name: "key-2")!
+  let key3 = EntryKey(name: "key-3")!
 
   let value1 = EntryValue(string: "value 1")!
   let value2 = EntryValue(string: "value 2")!