Google_Protobuf_Timestamp_Extensions.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // ProtobufRuntime/Sources/Protobuf/Google_Protobuf_Timestamp_Extensions.swift - Timestamp extensions
  2. //
  3. // This source file is part of the Swift.org open source project
  4. //
  5. // Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See http://swift.org/LICENSE.txt for license information
  9. // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
  10. //
  11. // -----------------------------------------------------------------------------
  12. ///
  13. /// Extend the generated Timestamp message with customized JSON coding,
  14. /// arithmetic operations, and convenience methods.
  15. ///
  16. // -----------------------------------------------------------------------------
  17. import Swift
  18. // TODO: Add convenience methods to interoperate with standard
  19. // date/time classes: an initializer that accepts Unix timestamp as
  20. // Int or Double, an easy way to convert to/from Foundation's
  21. // NSDateTime (on Apple platforms only?), others?
  22. private func FormatInt(n: Int32, digits: Int) -> String {
  23. if n < 0 {
  24. return FormatInt(n: -n, digits: digits)
  25. } else if digits <= 0 {
  26. return ""
  27. } else if digits == 1 && n < 10 {
  28. return ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"][Int(n)]
  29. } else {
  30. return FormatInt(n: n / 10, digits: digits - 1) + ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"][Int(n % 10)]
  31. }
  32. }
  33. private func fromAscii2(_ digit0: Int, _ digit1: Int) throws -> Int {
  34. let zero = Int(48)
  35. let nine = Int(57)
  36. if digit0 < zero || digit0 > nine || digit1 < zero || digit1 > nine {
  37. throw ProtobufDecodingError.malformedJSONTimestamp
  38. }
  39. return digit0 * 10 + digit1 - 528
  40. }
  41. private func fromAscii4(_ digit0: Int, _ digit1: Int, _ digit2: Int, _ digit3: Int) throws -> Int {
  42. let zero = Int(48)
  43. let nine = Int(57)
  44. if (digit0 < zero || digit0 > nine
  45. || digit1 < zero || digit1 > nine
  46. || digit2 < zero || digit2 > nine
  47. || digit3 < zero || digit3 > nine) {
  48. throw ProtobufDecodingError.malformedJSONTimestamp
  49. }
  50. return digit0 * 1000 + digit1 * 100 + digit2 * 10 + digit3 - 53328
  51. }
  52. // Parse an RFC3339 timestamp into a pair of seconds-since-1970 and nanos.
  53. private func parseTimestamp(s: String) throws -> (Int64, Int32) {
  54. // Convert to an array of integer character values
  55. let value = s.utf8.map{Int($0)}
  56. if value.count < 20 {
  57. throw ProtobufDecodingError.malformedJSONTimestamp
  58. }
  59. // Since the format is fixed-layout, we can just decode
  60. // directly as follows.
  61. let zero = Int(48)
  62. let nine = Int(57)
  63. let dash = Int(45)
  64. let colon = Int(58)
  65. let plus = Int(43)
  66. let letterT = Int(84)
  67. let letterZ = Int(90)
  68. let period = Int(46)
  69. // Year: 4 digits followed by '-'
  70. let year = try fromAscii4(value[0], value[1], value[2], value[3])
  71. if value[4] != dash || year < Int(1) || year > Int(9999) {
  72. throw ProtobufDecodingError.malformedJSONTimestamp
  73. }
  74. // Month: 2 digits followed by '-'
  75. let month = try fromAscii2(value[5], value[6])
  76. if value[7] != dash || month < Int(1) || month > Int(12) {
  77. throw ProtobufDecodingError.malformedJSONTimestamp
  78. }
  79. // Day: 2 digits followed by 'T'
  80. let mday = try fromAscii2(value[8], value[9])
  81. if value[10] != letterT || mday < Int(1) || mday > Int(31) {
  82. throw ProtobufDecodingError.malformedJSONTimestamp
  83. }
  84. // Hour: 2 digits followed by ':'
  85. let hour = try fromAscii2(value[11], value[12])
  86. if value[13] != colon || hour > Int(23) {
  87. throw ProtobufDecodingError.malformedJSONTimestamp
  88. }
  89. // Minute: 2 digits followed by ':'
  90. let minute = try fromAscii2(value[14], value[15])
  91. if value[16] != colon || minute > Int(59) {
  92. throw ProtobufDecodingError.malformedJSONTimestamp
  93. }
  94. // Second: 2 digits (following char is checked below)
  95. let second = try fromAscii2(value[17], value[18])
  96. if second > Int(61) {
  97. throw ProtobufDecodingError.malformedJSONTimestamp
  98. }
  99. // timegm() is almost entirely useless. It's nonexistent on
  100. // some platforms, broken on others. Everything else I've tried
  101. // is even worse. Hence the code below.
  102. // (If you have a better way to do this, try it and see if it
  103. // passes the test suite on both Linux and OS X.)
  104. // Day of year
  105. let mdayStart: [Int] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
  106. var yday = Int64(mdayStart[month - 1])
  107. let isleap = (year % 400 == 0) || ((year % 100 != 0) && (year % 4 == 0))
  108. if isleap && (month > 2) {
  109. yday += 1
  110. }
  111. yday += Int64(mday - 1)
  112. // Days since start of epoch (including leap days)
  113. var daysSinceEpoch = yday
  114. daysSinceEpoch += Int64(365 * year) - Int64(719527)
  115. daysSinceEpoch += Int64((year - 1) / 4)
  116. daysSinceEpoch -= Int64((year - 1) / 100)
  117. daysSinceEpoch += Int64((year - 1) / 400)
  118. // Second within day
  119. var daySec = Int64(hour)
  120. daySec *= 60
  121. daySec += Int64(minute)
  122. daySec *= 60
  123. daySec += Int64(second)
  124. // Seconds since start of epoch
  125. let t = daysSinceEpoch * Int64(86400) + daySec
  126. // After seconds, comes various optional bits
  127. var pos = 19
  128. var nanos: Int32 = 0
  129. if value[pos] == period { // "." begins fractional seconds
  130. pos += 1
  131. var digitValue = 100000000
  132. while pos < value.count && value[pos] >= zero && value[pos] <= nine {
  133. nanos += digitValue * (value[pos] - zero)
  134. digitValue /= 10
  135. pos += 1
  136. }
  137. }
  138. var seconds: Int64 = 0
  139. if value[pos] == plus || value[pos] == dash { // "+" or "-" starts Timezone offset
  140. if pos + 6 > value.count {
  141. throw ProtobufDecodingError.malformedJSONTimestamp
  142. }
  143. let hourOffset = try fromAscii2(value[pos + 1], value[pos + 2])
  144. let minuteOffset = try fromAscii2(value[pos + 4], value[pos + 5])
  145. if hourOffset > Int(13) || minuteOffset > Int(59) || value[pos + 3] != colon {
  146. throw ProtobufDecodingError.malformedJSONTimestamp
  147. }
  148. var adjusted: Int64 = t
  149. if value[pos] == plus {
  150. adjusted -= Int64(hourOffset) * Int64(3600)
  151. adjusted -= Int64(minuteOffset) * Int64(60)
  152. } else {
  153. adjusted += Int64(hourOffset) * Int64(3600)
  154. adjusted += Int64(minuteOffset) * Int64(60)
  155. }
  156. if adjusted < -62135596800 || adjusted > 253402300799 {
  157. throw ProtobufDecodingError.malformedJSONTimestamp
  158. }
  159. seconds = adjusted
  160. pos += 6
  161. } else if value[pos] == letterZ { // "Z" indicator for UTC
  162. seconds = t
  163. pos += 1
  164. } else {
  165. throw ProtobufDecodingError.malformedJSONTimestamp
  166. }
  167. if pos != value.count {
  168. throw ProtobufDecodingError.malformedJSONTimestamp
  169. }
  170. return (seconds, nanos)
  171. }
  172. private func formatTimestamp(seconds: Int64, nanos: Int32) -> String? {
  173. if ((seconds < 0 && nanos > 0)
  174. || (seconds > 0 && nanos < 0)
  175. || (seconds < -62135596800)
  176. || (seconds == -62135596800 && nanos < 0)
  177. || (seconds >= 253402300800)) {
  178. return nil
  179. }
  180. // Can't just use gmtime() here because time_t is sometimes 32 bits. Ugh.
  181. let secondsSinceStartOfDay = (Int32(seconds % 86400) + 86400) % 86400
  182. let sec = secondsSinceStartOfDay % 60
  183. let min = (secondsSinceStartOfDay / 60) % 60
  184. let hour = secondsSinceStartOfDay / 3600
  185. // The following implements Richards' algorithm (see the Wikipedia article
  186. // for "Julian day").
  187. // If you touch this code, please test it exhaustively by playing with
  188. // Test_Timestamp.testJSON_range.
  189. let julian = (seconds + 210866803200) / 86400
  190. let f = julian + 1401 + (((4 * julian + 274277) / 146097) * 3) / 4 - 38
  191. let e = 4 * f + 3
  192. let g = e % 1461 / 4
  193. let h = 5 * g + 2
  194. let mday = Int32(h % 153 / 5 + 1)
  195. let month = (h / 153 + 2) % 12 + 1
  196. let year = e / 1461 - 4716 + (12 + 2 - month) / 12
  197. // We can't use strftime here (it varies with locale)
  198. // We can't use strftime_l here (it's not portable)
  199. // The following is crude, but it works.
  200. // TODO: If String(format:) works, that might be even better
  201. // (it was broken on Linux a while back...)
  202. let result = (FormatInt(n: Int32(year), digits: 4)
  203. + "-"
  204. + FormatInt(n: Int32(month), digits: 2)
  205. + "-"
  206. + FormatInt(n: mday, digits: 2)
  207. + "T"
  208. + FormatInt(n: hour, digits: 2)
  209. + ":"
  210. + FormatInt(n: min, digits: 2)
  211. + ":"
  212. + FormatInt(n: sec, digits: 2))
  213. if nanos == 0 {
  214. return "\(result)Z"
  215. } else {
  216. var digits: Int
  217. var fraction: Int
  218. if nanos % 1000000 == 0 {
  219. fraction = Int(nanos) / 1000000
  220. digits = 3
  221. } else if nanos % 1000 == 0 {
  222. fraction = Int(nanos) / 1000
  223. digits = 6
  224. } else {
  225. fraction = Int(nanos)
  226. digits = 9
  227. }
  228. var formatted_fraction = ""
  229. while digits > 0 {
  230. formatted_fraction = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"][fraction % 10] + formatted_fraction
  231. fraction /= 10
  232. digits -= 1
  233. }
  234. return "\(result).\(formatted_fraction)Z"
  235. }
  236. }
  237. public extension Google_Protobuf_Timestamp {
  238. public init(secondsSinceEpoch: Int64, nanos: Int32 = 0) {
  239. self.init()
  240. self.seconds = secondsSinceEpoch
  241. self.nanos = nanos
  242. }
  243. public mutating func decodeFromJSONToken(token: ProtobufJSONToken) throws {
  244. if case .string(let s) = token {
  245. let timestamp = try parseTimestamp(s: s)
  246. seconds = timestamp.0
  247. nanos = timestamp.1
  248. } else {
  249. throw ProtobufDecodingError.schemaMismatch
  250. }
  251. }
  252. public func serializeJSON() throws -> String {
  253. let s = seconds
  254. let n = nanos
  255. if let formatted = formatTimestamp(seconds: s, nanos: n) {
  256. return "\"\(formatted)\""
  257. } else {
  258. throw ProtobufEncodingError.timestampJSONRange
  259. }
  260. }
  261. func serializeAnyJSON() throws -> String {
  262. let value = try serializeJSON()
  263. return "{\"@type\":\"\(anyTypeURL)\",\"value\":\(value)}"
  264. }
  265. }
  266. private func normalizedTimestamp(seconds: Int64, nanos: Int32) -> Google_Protobuf_Timestamp {
  267. var s = seconds
  268. var n = nanos
  269. if n >= 1000000000 || n <= -1000000000 {
  270. s += Int64(n) / 1000000000
  271. n = n % 1000000000
  272. }
  273. if s > 0 && n < 0 {
  274. n += 1000000000
  275. s -= 1
  276. } else if s < 0 && n > 0 {
  277. n -= 1000000000
  278. s += 1
  279. }
  280. return Google_Protobuf_Timestamp(seconds: s, nanos: n)
  281. }
  282. public func -(lhs: Google_Protobuf_Timestamp, rhs: Google_Protobuf_Duration) -> Google_Protobuf_Timestamp {
  283. return normalizedTimestamp(seconds: lhs.seconds - rhs.seconds, nanos: lhs.nanos - rhs.nanos)
  284. }
  285. public func+(lhs: Google_Protobuf_Timestamp, rhs: Google_Protobuf_Duration) -> Google_Protobuf_Timestamp {
  286. return normalizedTimestamp(seconds: lhs.seconds + rhs.seconds, nanos: lhs.nanos + rhs.nanos)
  287. }