RadrootsGovernedFileReaderTests.swift (7988B)
1 import Darwin 2 import Foundation 3 import Testing 4 5 @testable import RadrootsKit 6 7 @Test func governedFileReaderReadsAnExactBoundedRegularFile() throws { 8 try withGovernedFileFixture { root in 9 let directory = root.appendingPathComponent("config", isDirectory: true) 10 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) 11 let payload = Data("governed".utf8) 12 try payload.write(to: directory.appendingPathComponent("control.json")) 13 14 #expect( 15 try RadrootsGovernedFileReader.read( 16 root: root, 17 relativePath: "config/control.json", 18 maximumBytes: payload.count 19 ) == payload 20 ) 21 } 22 } 23 24 @Test(arguments: ["", "/absolute", ".", "..", "config/", "config//control", "config/../control"]) 25 func governedFileReaderRejectsInvalidRelativePaths(_ relativePath: String) throws { 26 try withGovernedFileFixture { root in 27 #expect(throws: RadrootsGovernedFileReadError.invalidRequest) { 28 _ = try RadrootsGovernedFileReader.read( 29 root: root, 30 relativePath: relativePath, 31 maximumBytes: 16 32 ) 33 } 34 } 35 } 36 37 @Test func governedFileReaderRejectsRootIntermediateAndLeafSymlinks() throws { 38 try withGovernedFileFixture { root in 39 let actual = root.appendingPathComponent("actual", isDirectory: true) 40 try FileManager.default.createDirectory(at: actual, withIntermediateDirectories: false) 41 try Data("value".utf8).write(to: actual.appendingPathComponent("control")) 42 43 let rootLink = root.deletingLastPathComponent() 44 .appendingPathComponent("\(root.lastPathComponent)-link") 45 defer { try? FileManager.default.removeItem(at: rootLink) } 46 try FileManager.default.createSymbolicLink(at: rootLink, withDestinationURL: root) 47 #expect(throws: RadrootsGovernedFileReadError.invalidObject) { 48 _ = try RadrootsGovernedFileReader.read( 49 root: rootLink, 50 relativePath: "actual/control", 51 maximumBytes: 16 52 ) 53 } 54 55 let directoryLink = root.appendingPathComponent("directory-link") 56 try FileManager.default.createSymbolicLink(at: directoryLink, withDestinationURL: actual) 57 #expect(throws: RadrootsGovernedFileReadError.invalidObject) { 58 _ = try RadrootsGovernedFileReader.read( 59 root: root, 60 relativePath: "directory-link/control", 61 maximumBytes: 16 62 ) 63 } 64 65 let leafLink = root.appendingPathComponent("leaf-link") 66 try FileManager.default.createSymbolicLink( 67 at: leafLink, 68 withDestinationURL: actual.appendingPathComponent("control") 69 ) 70 #expect(throws: RadrootsGovernedFileReadError.invalidObject) { 71 _ = try RadrootsGovernedFileReader.read( 72 root: root, 73 relativePath: "leaf-link", 74 maximumBytes: 16 75 ) 76 } 77 } 78 } 79 80 @Test func governedFileReaderRejectsDirectoriesFifosAndOversizedFiles() throws { 81 try withGovernedFileFixture { root in 82 let directory = root.appendingPathComponent("directory", isDirectory: true) 83 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) 84 #expect(throws: RadrootsGovernedFileReadError.invalidObject) { 85 _ = try RadrootsGovernedFileReader.read( 86 root: root, 87 relativePath: "directory", 88 maximumBytes: 16 89 ) 90 } 91 92 let fifo = root.appendingPathComponent("control.fifo") 93 let fifoResult = fifo.path.withCString { Darwin.mkfifo($0, S_IRUSR | S_IWUSR) } 94 #expect(fifoResult == 0) 95 #expect(throws: RadrootsGovernedFileReadError.invalidObject) { 96 _ = try RadrootsGovernedFileReader.read( 97 root: root, 98 relativePath: "control.fifo", 99 maximumBytes: 16 100 ) 101 } 102 103 try Data(repeating: 0x41, count: 17).write(to: root.appendingPathComponent("oversized")) 104 #expect(throws: RadrootsGovernedFileReadError.tooLarge) { 105 _ = try RadrootsGovernedFileReader.read( 106 root: root, 107 relativePath: "oversized", 108 maximumBytes: 16 109 ) 110 } 111 } 112 } 113 114 @Test func governedFileReaderRejectsLeafReplacementAfterAdmission() throws { 115 try withGovernedFileFixture { root in 116 let file = root.appendingPathComponent("control") 117 let retained = root.appendingPathComponent("retained") 118 try Data("original".utf8).write(to: file) 119 120 #expect(throws: RadrootsGovernedFileReadError.changedDuringRead) { 121 _ = try RadrootsGovernedFileReader.readForTesting( 122 root: root, 123 relativePath: "control", 124 maximumBytes: 16 125 ) { 126 try FileManager.default.moveItem(at: file, to: retained) 127 try Data("foreign".utf8).write(to: file) 128 } 129 } 130 } 131 } 132 133 @Test func governedFileReaderRejectsDirectoryReplacementAfterAdmission() throws { 134 try withGovernedFileFixture { root in 135 let directory = root.appendingPathComponent("config", isDirectory: true) 136 let retained = root.appendingPathComponent("retained", isDirectory: true) 137 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false) 138 try Data("original".utf8).write(to: directory.appendingPathComponent("control")) 139 140 #expect(throws: RadrootsGovernedFileReadError.changedDuringRead) { 141 _ = try RadrootsGovernedFileReader.readForTesting( 142 root: root, 143 relativePath: "config/control", 144 maximumBytes: 16 145 ) { 146 try FileManager.default.moveItem(at: directory, to: retained) 147 try FileManager.default.createDirectory( 148 at: directory, withIntermediateDirectories: false) 149 try Data("foreign".utf8).write(to: directory.appendingPathComponent("control")) 150 } 151 } 152 } 153 } 154 155 @Test func governedFileReaderRejectsInPlaceMutationAfterAdmission() throws { 156 try withGovernedFileFixture { root in 157 let file = root.appendingPathComponent("control") 158 try Data("original".utf8).write(to: file) 159 160 #expect(throws: RadrootsGovernedFileReadError.changedDuringRead) { 161 _ = try RadrootsGovernedFileReader.readForTesting( 162 root: root, 163 relativePath: "control", 164 maximumBytes: 16 165 ) { 166 try Data("mutated-longer".utf8).write(to: file) 167 } 168 } 169 } 170 } 171 172 @Test func governedFileReaderErrorsContainNoPathOrContent() throws { 173 let canaries = ["/private/sensitive/control.json", "secret-canary"] 174 let errors: [RadrootsGovernedFileReadError] = [ 175 .invalidRequest, 176 .unavailable, 177 .invalidObject, 178 .tooLarge, 179 .changedDuringRead, 180 .ioFailure, 181 ] 182 for error in errors { 183 let rendered = String(reflecting: error) 184 #expect(canaries.allSatisfy { !rendered.contains($0) }) 185 } 186 } 187 188 private func withGovernedFileFixture( 189 _ body: (URL) throws -> Void 190 ) throws { 191 let unresolvedRoot = FileManager.default.temporaryDirectory 192 .appendingPathComponent("radroots-governed-file-\(UUID().uuidString)", isDirectory: true) 193 try FileManager.default.createDirectory(at: unresolvedRoot, withIntermediateDirectories: false) 194 guard let resolvedPointer = unresolvedRoot.path.withCString({ Darwin.realpath($0, nil) }) else { 195 throw RadrootsGovernedFileReadError.ioFailure 196 } 197 defer { Darwin.free(resolvedPointer) } 198 let root = URL(fileURLWithPath: String(cString: resolvedPointer), isDirectory: true) 199 defer { try? FileManager.default.removeItem(at: root) } 200 try body(root) 201 }