1 //  Copyright © 2021 650 Industries. All rights reserved.
2 
3 import Foundation
4 
5 public enum SerializerError: Error {
6   case emptyKey
7   case invalidCharacterInKey(key: String, character: Character)
8   case invalidCharacterInString(string: String, character: Character)
9 }
10 
11 private extension Character {
fromHexnull12   static func fromHex(_ hex: String) -> Character {
13     // swiftlint:disable:next force_unwrapping
14     return Character(Unicode.Scalar(Int(hex, radix: 16)!)!)
15   }
16 
17   var isDigit: Bool {
18     return self >= Character("0") && self <= Character("9")
19   }
20 
21   var isLcAlpha: Bool {
22     self >= Character("a") && self <= Character("z")
23   }
24 
25   var isAlpha: Bool {
26     return (self >= Character("A") && self <= Character("Z")) || isLcAlpha
27   }
28 }
29 
30 /**
31  Derived from expo-structured-headers Android implementation.
32  */
33 public class StringStringDictionarySerializer {
checkKeynull34   private static func checkKey(key: String) throws {
35     guard !key.isEmpty else {
36       throw SerializerError.emptyKey
37     }
38 
39     for (i, c) in key.enumerated() {
40       let failureCondition1 = i == 0 && (c != Character("*") && !c.isLcAlpha)
41       let failureCondition2 = !(c.isLcAlpha || c.isDigit || c == Character("_") || c == Character("-") || c == Character(".") || c == Character("*"))
42       if failureCondition1 || failureCondition2 {
43         throw SerializerError.invalidCharacterInKey(key: key, character: c)
44       }
45     }
46   }
47 
checkValuenull48   private static func checkValue(value: String) throws {
49     for c in value {
50       if c < Character.fromHex("20") || c >= Character.fromHex("7f") {
51         throw SerializerError.invalidCharacterInString(string: value, character: c)
52       }
53     }
54   }
55 
serializeValuenull56   private static func serializeValue(value: String) -> String {
57     let escapedString = value.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
58     return "\"\(escapedString)\""
59   }
60 
serializenull61   public static func serialize(dictionary: [String: String]) throws -> String {
62     return try dictionary
63       .map { (key: String, value: String) in
64         try checkKey(key: key)
65         try checkValue(value: value)
66         return "\(key)=\(serializeValue(value: value))"
67       }
68       .joined(separator: ", ")
69   }
70 }
71