summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadREST/Relay/RelayCache.swift
blob: 0c2b0bb34527f335a00b2d8ab3b5153539df36ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
//
//  RelayCache.swift
//  RelayCache
//
//  Created by pronebird on 06/09/2021.
//  Copyright © 2025 Mullvad VPN AB. All rights reserved.
//

import Foundation
import MullvadTypes

public protocol RelayCacheProtocol: Sendable {
    /// Reads from a cached list,
    /// which falls back to reading from prebundled relays if there was no cache hit
    func read() throws -> StoredRelays
    /// Reads the relays file that were prebundled with the app installation.
    ///
    /// > Warning: Prefer `read()` over this unless there is an explicit need to read
    /// relays from the bundle, because those might contain stale data.
    func readPrebundledRelays() throws -> StoredRelays
    func write(record: StoredRelays) throws
}

/// - Warning: `RelayCache` should not be used directly. It should be used through `IPOverrideWrapper` to have
/// ip overrides applied.
public final class RelayCache: RelayCacheProtocol, Sendable {
    private let fileURL: URL
    nonisolated(unsafe) private let fileCache: any FileCacheProtocol<StoredRelays>

    /// Designated initializer
    public init(cacheDirectory: URL) {
        fileURL = cacheDirectory.appendingPathComponent("relays.json", isDirectory: false)
        fileCache = FileCache(fileURL: fileURL)
    }

    /// Initializer that accepts a custom FileCache implementation. Used in tests.
    init(fileCache: some FileCacheProtocol<StoredRelays>) {
        fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("relays.json", isDirectory: false)
        self.fileCache = fileCache
    }

    /// Safely read the cache file from disk using file coordinator and fallback in the following manner:
    /// 1. If there is a file but it's not decodable, try to parse into the old cache format. If it's still
    ///    not decodable, read the pre-bundled data.
    /// 2. If there is no file, read from the pre-bundled data.
    public func read() throws -> StoredRelays {
        do {
            return try fileCache.read()
        } catch is DecodingError {
            do {
                let oldFormatFileCache = FileCache<CachedRelays>(fileURL: fileURL)
                return try StoredRelays(cachedRelays: try oldFormatFileCache.read())
            } catch {
                return try readPrebundledRelays()
            }
        } catch {
            return try readPrebundledRelays()
        }
    }

    /// Safely write the cache file on disk using file coordinator.
    public func write(record: StoredRelays) throws {
        try fileCache.write(record)
    }

    /// Read pre-bundled relays file from disk.
    public func readPrebundledRelays() throws -> StoredRelays {
        guard let prebundledRelaysFileURL = Bundle(for: Self.self).url(forResource: "relays", withExtension: "json")
        else { throw CocoaError(.fileNoSuchFile) }

        let data = try Data(contentsOf: prebundledRelaysFileURL)

        return try StoredRelays(
            rawData: data,
            updatedAt: Date(timeIntervalSince1970: 0)
        )
    }
}