diff --git a/samhuri.net/Sources/samhuri.net/Dates/Date+Sugar.swift b/samhuri.net/Sources/samhuri.net/Dates/Date+Sugar.swift new file mode 100644 index 0000000..13db930 --- /dev/null +++ b/samhuri.net/Sources/samhuri.net/Dates/Date+Sugar.swift @@ -0,0 +1,24 @@ +// +// Date+Sugar.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-19. +// + +import Foundation + +extension Date { + static var defaultCalendar = Calendar.current + + var year: Int { + Date.defaultCalendar.dateComponents([.year], from: self).year! + } + + var month: Int { + Date.defaultCalendar.dateComponents([.month], from: self).month! + } + + var day: Int { + Date.defaultCalendar.dateComponents([.day], from: self).day! + } +} diff --git a/samhuri.net/Sources/samhuri.net/Extensions/Date+Sugar.swift b/samhuri.net/Sources/samhuri.net/Extensions/Date+Sugar.swift deleted file mode 100644 index 1483e3c..0000000 --- a/samhuri.net/Sources/samhuri.net/Extensions/Date+Sugar.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Date+Sugar.swift -// samhuri.net -// -// Created by Sami Samhuri on 2019-12-19. -// - -import Foundation - -extension Date { - var year: Int { - Calendar.current.dateComponents([.year], from: self).year! - } - - var month: Int { - Calendar.current.dateComponents([.month], from: self).month! - } - - var day: Int { - Calendar.current.dateComponents([.day], from: self).day! - } -} diff --git a/samhuri.net/Sources/samhuri.net/Files/DirectoryCreating.swift b/samhuri.net/Sources/samhuri.net/Files/DirectoryCreating.swift index 4d6f15f..e0aa098 100644 --- a/samhuri.net/Sources/samhuri.net/Files/DirectoryCreating.swift +++ b/samhuri.net/Sources/samhuri.net/Files/DirectoryCreating.swift @@ -13,6 +13,8 @@ protocol DirectoryCreating { extension FileManager: DirectoryCreating { func createDirectory(at url: URL) throws { - try createDirectory(at: url, withIntermediateDirectories: true, attributes: [.posixPermissions: FilePermissions.directoryDefault.rawValue]) + try createDirectory(at: url, withIntermediateDirectories: true, attributes: [ + .posixPermissions: FilePermissions.directoryDefault.rawValue, + ]) } } diff --git a/samhuri.net/Sources/samhuri.net/Files/FilePermissions.swift b/samhuri.net/Sources/samhuri.net/Files/FilePermissions.swift index 98f3b87..e72faa4 100644 --- a/samhuri.net/Sources/samhuri.net/Files/FilePermissions.swift +++ b/samhuri.net/Sources/samhuri.net/Files/FilePermissions.swift @@ -7,7 +7,7 @@ import Foundation -struct FilePermissions: CustomStringConvertible { +struct FilePermissions: Equatable, CustomStringConvertible { let user: Permissions let group: Permissions let other: Permissions @@ -16,15 +16,22 @@ struct FilePermissions: CustomStringConvertible { [user, group, other].map { $0.description }.joined() } - static let `default`: FilePermissions = "rw-r--r--" + static let fileDefault: FilePermissions = "rw-r--r--" static let directoryDefault: FilePermissions = "rwxr-xr-x" } extension FilePermissions { - init(string: String) { - user = Permissions(string: String(string.prefix(3))) - group = Permissions(string: String(string.dropFirst(3).prefix(3))) - other = Permissions(string: String(string.dropFirst(6).prefix(3))) + init?(string: String) { + guard let user = Permissions(string: String(string.prefix(3))), + let group = Permissions(string: String(string.dropFirst(3).prefix(3))), + let other = Permissions(string: String(string.dropFirst(6).prefix(3))) + else { + return nil + } + + self.user = user + self.group = group + self.other = other } } @@ -34,14 +41,14 @@ extension FilePermissions: RawRepresentable { } init(rawValue: Int16) { - user = Permissions(rawValue: rawValue >> 6 & 7) - group = Permissions(rawValue: rawValue >> 3 & 7) - other = Permissions(rawValue: rawValue >> 0 & 7) + user = Permissions(rawValue: rawValue >> 6 & 0b111) + group = Permissions(rawValue: rawValue >> 3 & 0b111) + other = Permissions(rawValue: rawValue >> 0 & 0b111) } } extension FilePermissions: ExpressibleByStringLiteral { init(stringLiteral value: String) { - self.init(string: value) + self.init(string: value)! } } diff --git a/samhuri.net/Sources/samhuri.net/Files/FileWriting.swift b/samhuri.net/Sources/samhuri.net/Files/FileWriting.swift index 00e82a7..f941abf 100644 --- a/samhuri.net/Sources/samhuri.net/Files/FileWriting.swift +++ b/samhuri.net/Sources/samhuri.net/Files/FileWriting.swift @@ -17,10 +17,10 @@ protocol FileWriting { extension FileWriting { func write(data: Data, to fileURL: URL) throws { - try write(data: data, to: fileURL, permissions: .default) + try write(data: data, to: fileURL, permissions: .fileDefault) } func write(string: String, to fileURL: URL) throws { - try write(string: string, to: fileURL, permissions: .default) + try write(string: string, to: fileURL, permissions: .fileDefault) } } diff --git a/samhuri.net/Sources/samhuri.net/Files/Permissions.swift b/samhuri.net/Sources/samhuri.net/Files/Permissions.swift index 8372b58..8380c5a 100644 --- a/samhuri.net/Sources/samhuri.net/Files/Permissions.swift +++ b/samhuri.net/Sources/samhuri.net/Files/Permissions.swift @@ -10,6 +10,10 @@ import Foundation struct Permissions: OptionSet { let rawValue: Int16 + static let none = Permissions(rawValue: 0) + + // These raw values match those used by Unix file systems and must not be changed. + static let execute = Permissions(rawValue: 1 << 0) static let write = Permissions(rawValue: 1 << 1) static let read = Permissions(rawValue: 1 << 2) @@ -18,16 +22,34 @@ struct Permissions: OptionSet { self.rawValue = rawValue } - init(string: String) { + init?(string: String) { self.init(rawValue: 0) - if string[string.startIndex] == "r" { + + switch string[string.startIndex] { + case "r": insert(.read) + case "-": + break + default: + return nil } - if string[string.index(string.startIndex, offsetBy: 1)] == "w" { + + switch string[string.index(string.startIndex, offsetBy: 1)] { + case "w": insert(.write) + case "-": + break + default: + return nil } - if string[string.index(string.startIndex, offsetBy: 2)] == "x" { + + switch string[string.index(string.startIndex, offsetBy: 2)] { + case "x": insert(.execute) + case "-": + break + default: + return nil } } } @@ -44,6 +66,6 @@ extension Permissions: CustomStringConvertible { extension Permissions: ExpressibleByStringLiteral { init(stringLiteral value: String) { - self.init(string: value) + self.init(string: value)! } } diff --git a/samhuri.net/Tests/samhuri.netTests/Dates/Date+SugarTests.swift b/samhuri.net/Tests/samhuri.netTests/Dates/Date+SugarTests.swift new file mode 100644 index 0000000..aa56b91 --- /dev/null +++ b/samhuri.net/Tests/samhuri.netTests/Dates/Date+SugarTests.swift @@ -0,0 +1,45 @@ +// +// Date+SugarTests.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-31. +// + +import XCTest +@testable import samhuri_net + +extension Date { + final class Tests: XCTestCase { + var date: Date! + + override func setUp() { + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = TimeZone(secondsFromGMT: 0)! + Date.defaultCalendar = calendar + date = Date(timeIntervalSince1970: 0) + } + + override func tearDown() { + Date.defaultCalendar = .current + date = nil + } + + func testYear() { + XCTAssertEqual(1970, date.year) + } + + func testMonth() { + XCTAssertEqual(1, date.month) + } + + func testDay() { + XCTAssertEqual(1, date.day) + } + + static var allTests = [ + ("testYear", testYear), + ("testMonth", testMonth), + ("testDay", testDay), + ] + } +} diff --git a/samhuri.net/Tests/samhuri.netTests/Files/FilePermissionsTests.swift b/samhuri.net/Tests/samhuri.netTests/Files/FilePermissionsTests.swift new file mode 100644 index 0000000..6c7c4a5 --- /dev/null +++ b/samhuri.net/Tests/samhuri.netTests/Files/FilePermissionsTests.swift @@ -0,0 +1,63 @@ +// +// FilePermissionsTests.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-31. +// + +import XCTest +@testable import samhuri_net + +extension FilePermissions { + final class Tests: XCTestCase { + func testDescription() { + XCTAssertEqual("---------", FilePermissions(user: "---", group: "---", other: "---").description) + XCTAssertEqual("r--r--r--", FilePermissions(user: "r--", group: "r--", other: "r--").description) + XCTAssertEqual("-w--w--w-", FilePermissions(user: "-w-", group: "-w-", other: "-w-").description) + XCTAssertEqual("--x--x--x", FilePermissions(user: "--x", group: "--x", other: "--x").description) + XCTAssertEqual("rwxr-xr--", FilePermissions(user: "rwx", group: "r-x", other: "r--").description) + } + + func testInitFromString() { + XCTAssertEqual(FilePermissions(user: "---", group: "---", other: "---"), FilePermissions(string: "---------")) + XCTAssertEqual(FilePermissions(user: "r--", group: "r--", other: "r--"), FilePermissions(string: "r--r--r--")) + XCTAssertEqual(FilePermissions(user: "-w-", group: "-w-", other: "-w-"), FilePermissions(string: "-w--w--w-")) + XCTAssertEqual(FilePermissions(user: "--x", group: "--x", other: "--x"), FilePermissions(string: "--x--x--x")) + XCTAssertEqual(FilePermissions(user: "rwx", group: "r-x", other: "r--"), FilePermissions(string: "rwxr-xr--")) + + // Refuses to initialize with nonsense. + XCTAssertNil(FilePermissions(string: "abcdefghi")) + XCTAssertNil(FilePermissions(string: "abcrwxrwx")) + XCTAssertNil(FilePermissions(string: "rwxabcrwx")) + XCTAssertNil(FilePermissions(string: "rwxrwxabc")) + } + + func testInitFromRawValue() { + XCTAssertEqual("---------", FilePermissions(rawValue: 0o000)) + XCTAssertEqual("rwxr-xr-x", FilePermissions(rawValue: 0o755)) + XCTAssertEqual("rw-r--r--", FilePermissions(rawValue: 0o644)) + XCTAssertEqual("rw-------", FilePermissions(rawValue: 0o600)) + XCTAssertEqual("rwxrwxrwx", FilePermissions(rawValue: 0o777)) + } + + func testRawValue() { + XCTAssertEqual(0o000, FilePermissions(string: "---------")!.rawValue) + XCTAssertEqual(0o755, FilePermissions(string: "rwxr-xr-x")!.rawValue) + XCTAssertEqual(0o644, FilePermissions(string: "rw-r--r--")!.rawValue) + XCTAssertEqual(0o600, FilePermissions(string: "rw-------")!.rawValue) + XCTAssertEqual(0o777, FilePermissions(string: "rwxrwxrwx")!.rawValue) + } + + func testExpressibleByStringLiteral() { + XCTAssertEqual(FilePermissions(user: "rwx", group: "r-x", other: "r-x"), "rwxr-xr-x") + } + + static var allTests = [ + ("testDescription", testDescription), + ("testInitFromString", testInitFromString), + ("testInitFromRawValue", testInitFromRawValue), + ("testRawValue", testRawValue), + ("testExpressibleByStringLiteral", testExpressibleByStringLiteral), + ] + } +} diff --git a/samhuri.net/Tests/samhuri.netTests/Files/PermissionsTests.swift b/samhuri.net/Tests/samhuri.netTests/Files/PermissionsTests.swift new file mode 100644 index 0000000..56cdf51 --- /dev/null +++ b/samhuri.net/Tests/samhuri.netTests/Files/PermissionsTests.swift @@ -0,0 +1,67 @@ +// +// PermissionsTests.swift +// samhuri.net +// +// Created by Sami Samhuri on 2019-12-31. +// + +import XCTest +@testable import samhuri_net + +extension Permissions { + final class Tests: XCTestCase { + func testOptionsAreMutuallyExclusive() { + // If any of the bits overlap then the `or` value will be less than the sum of the raw values. + let allValues = [Permissions.execute, Permissions.write, Permissions.read].map { $0.rawValue } + XCTAssertEqual(allValues.reduce(0, +), allValues.reduce(0, |)) + } + + func testRawValuesAreUnixy() { + XCTAssertEqual(0o0, Permissions.none.rawValue) + XCTAssertEqual(0o4, Permissions.read.rawValue) + XCTAssertEqual(0o2, Permissions.write.rawValue) + XCTAssertEqual(0o1, Permissions.execute.rawValue) + } + + func testInitFromString() { + XCTAssertEqual([.none], Permissions(string: "---")) + XCTAssertEqual([.execute], Permissions(string: "--x")) + XCTAssertEqual([.write], Permissions(string: "-w-")) + XCTAssertEqual([.read], Permissions(string: "r--")) + + XCTAssertEqual([.read, .write], Permissions(string: "rw-")) + XCTAssertEqual([.read, .execute], Permissions(string: "r-x")) + XCTAssertEqual([.write, .execute], Permissions(string: "-wx")) + XCTAssertEqual([.read, .write, .execute], Permissions(string: "rwx")) + + // Refuses to initialize with nonsense. + XCTAssertNil(Permissions(string: "abc")) + XCTAssertNil(Permissions(string: "awx")) + XCTAssertNil(Permissions(string: "rax")) + XCTAssertNil(Permissions(string: "rwa")) + } + + func testDescription() { + XCTAssertEqual("---", Permissions.none.description) + XCTAssertEqual("r--", Permissions.read.description) + XCTAssertEqual("-w-", Permissions.write.description) + XCTAssertEqual("--x", Permissions.execute.description) + XCTAssertEqual("rw-", Permissions(arrayLiteral: [.read, .write]).description) + XCTAssertEqual("r-x", Permissions(arrayLiteral: [.read, .execute]).description) + XCTAssertEqual("-wx", Permissions(arrayLiteral: [.write, .execute]).description) + XCTAssertEqual("rwx", Permissions(arrayLiteral: [.read, .write, .execute]).description) + } + + func testExpressibleByStringLiteral() { + XCTAssertEqual(Permissions.read, "r--") + } + + static var allTests = [ + ("testOptionsAreMutuallyExclusive", testOptionsAreMutuallyExclusive), + ("testRawValuesAreUnixy", testRawValuesAreUnixy), + ("testInitFromString", testInitFromString), + ("testDescription", testDescription), + ("testExpressibleByStringLiteral", testExpressibleByStringLiteral), + ] + } +} diff --git a/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift b/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift index b619c5a..ff4244e 100644 --- a/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift +++ b/samhuri.net/Tests/samhuri.netTests/XCTestManifests.swift @@ -3,6 +3,9 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ + testCase(Date.Tests.allTests), + testCase(Permissions.Tests.allTests), + testCase(FilePermissions.Tests.allTests), testCase(samhuri.net.Tests.allTests), ] }