krz/keycask

Password manager: Swift core library, CLI for macOS/Linux/Windows, iOS/macOS app. cli password-manager swift

Sources/KeycaskCore/Entry.swift

65 lines · 2008 bytes

 1import Foundation
 2
 3public struct Entry: Codable, Equatable, Sendable {
 4    public let id: EntryID
 5    public var name: String
 6    public var username: String?
 7    public var password: String
 8    public var url: String?
 9    public var notes: String?
10    public var tags: [String]
11    public let created: Date
12    public var updated: Date
13
14    public init(
15        id: EntryID = .random(),
16        name: String,
17        username: String? = nil,
18        password: String,
19        url: String? = nil,
20        notes: String? = nil,
21        tags: [String] = [],
22        now: Date = .now
23    ) {
24        self.id = id
25        self.name = name
26        self.username = username
27        self.password = password
28        self.url = url
29        self.notes = notes
30        self.tags = Self.normalize(tags: tags)
31        let stamp = Self.truncateToSeconds(now)
32        created = stamp
33        updated = stamp
34    }
35
36    public static func normalize(tags: [String]) -> [String] {
37        var seen: Set<String> = []
38        var out: [String] = []
39        for raw in tags {
40            let tag = raw.trimmingCharacters(in: .whitespaces)
41            guard !tag.isEmpty, seen.insert(tag.lowercased()).inserted else { continue }
42            out.append(tag)
43        }
44        return out.sorted { a, b in
45            let (la, lb) = (a.lowercased(), b.lowercased())
46            return la == lb ? a < b : la < lb
47        }
48    }
49
50    public static func truncateToSeconds(_ date: Date) -> Date {
51        Date(timeIntervalSince1970: date.timeIntervalSince1970.rounded(.down))
52    }
53
54    public func hasTag(_ tag: String) -> Bool {
55        let needle = tag.lowercased()
56        return tags.contains { $0.lowercased() == needle }
57    }
58
59    public func matches(_ query: String) -> Bool {
60        let needle = query.lowercased()
61        guard !needle.isEmpty else { return false }
62        let haystacks = [name, username ?? "", url ?? "", notes ?? ""] + tags
63        return haystacks.contains { $0.lowercased().contains(needle) }
64    }
65}