HashCalculator.swift 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import CommonCrypto
  17. import Foundation
  18. /// Hashing related utility functions. The enum type is used as a namespace here instead of having
  19. /// root functions, and no cases should be added to it. Note: this would be named `Hasher` but it
  20. /// collide's with Foundation's `Hasher` type.
  21. enum HashCalculator {}
  22. extension HashCalculator {
  23. enum HashError: Error {
  24. /// Real errors aren't thrown, so just give text what happened.
  25. case failed(String)
  26. }
  27. /// Hashes the contents of the directory recursively.
  28. static func sha256Contents(ofDir dir: URL) throws -> String {
  29. var hashes: [String] = []
  30. let allContents = try FileManager.default.recursivelySearch(for: .allFiles, in: dir)
  31. // Sort the contents to make it deterministic.
  32. let sortedContents = allContents.sorted { $0.absoluteString < $1.absoluteString }
  33. for file in sortedContents {
  34. // Hash the contents of the file.
  35. let contentsHash = try sha256(file)
  36. hashes.append(contentsHash)
  37. // Hash the file name as well.
  38. let nameHash = try sha256(file.path)
  39. hashes.append(nameHash)
  40. }
  41. if hashes.isEmpty {
  42. throw HashError.failed("Directory \(dir) does not contain any files.")
  43. }
  44. // Calculate the final hash by hashing all the hashes joined together.
  45. let hash = try sha256(hashes.joined())
  46. return hash
  47. }
  48. /// Calculates the SHA256 hash of the data given.
  49. static func sha256(_ data: Data) -> String {
  50. var digest = [UInt8].init(repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
  51. #if swift(>=5)
  52. _ = data.withUnsafeBytes {
  53. CC_SHA256($0.baseAddress, UInt32(data.count), &digest)
  54. }
  55. #else
  56. _ = data.withUnsafeBytes {
  57. CC_SHA256($0, UInt32(data.count), &digest)
  58. }
  59. #endif
  60. let characters = digest.map { String(format: "%02x", $0) }
  61. return characters.joined()
  62. }
  63. /// Calculates the SHA256 hash of the contents of the file at the given URL.
  64. static func sha256(_ file: URL) throws -> String {
  65. guard file.isFileURL else {
  66. throw HashError.failed("URL given is not a file URL. \(file)")
  67. }
  68. guard let data = FileManager.default.contents(atPath: file.path) else {
  69. throw HashError.failed("Could not get data from \(file.path).")
  70. }
  71. return sha256(data)
  72. }
  73. /// Calculates the SHA256 hash of the text given.
  74. static func sha256(_ text: String) throws -> String {
  75. // If we can't get UTF8 bytes out, return nil.
  76. guard let data = text.data(using: .utf8) else {
  77. throw HashError.failed("String used was not UTF8 compliant. \"\(text)\"")
  78. }
  79. return sha256(data)
  80. }
  81. }