From 53c231f4518a6707b08020187a7c1699023deb87 Mon Sep 17 00:00:00 2001 From: Joannis Orlandos Date: Sat, 28 Dec 2024 08:50:00 +0100 Subject: [PATCH] Follow symlinks when reading a file's attributes (#642) * Follow symlinks when reading a file's attributes * Fix up the formatting --- Sources/Hummingbird/Files/FileIO.swift | 16 +++++++- .../Hummingbird/Files/LocalFileSystem.swift | 10 ++--- .../FileMiddlewareTests.swift | 38 +++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Sources/Hummingbird/Files/FileIO.swift b/Sources/Hummingbird/Files/FileIO.swift index 46620188..d0c727ff 100644 --- a/Sources/Hummingbird/Files/FileIO.swift +++ b/Sources/Hummingbird/Files/FileIO.swift @@ -42,7 +42,7 @@ public struct FileIO: Sendable { chunkLength: Int = NonBlockingFileIO.defaultChunkSize ) async throws -> ResponseBody { do { - let stat = try await fileIO.lstat(path: path) + let stat = try await fileIO.stat(path: path) guard stat.st_size > 0 else { return .init() } return self.readFile(path: path, range: 0...numericCast(stat.st_size - 1), context: context, chunkLength: chunkLength) } catch { @@ -67,7 +67,7 @@ public struct FileIO: Sendable { chunkLength: Int = NonBlockingFileIO.defaultChunkSize ) async throws -> ResponseBody { do { - let stat = try await fileIO.lstat(path: path) + let stat = try await fileIO.stat(path: path) guard stat.st_size > 0 else { return .init() } let fileRange: ClosedRange = 0...numericCast(stat.st_size - 1) let range = range.clamped(to: fileRange) @@ -144,3 +144,15 @@ public struct FileIO: Sendable { } } } + +extension NonBlockingFileIO { + func stat(path: String) async throws -> stat { + let stat = try await self.lstat(path: path) + if stat.st_mode & S_IFMT == S_IFLNK { + let realPath = try await self.readlink(path: path) + return try await self.lstat(path: realPath) + } else { + return stat + } + } +} diff --git a/Sources/Hummingbird/Files/LocalFileSystem.swift b/Sources/Hummingbird/Files/LocalFileSystem.swift index 2ab4937a..d0099fb0 100644 --- a/Sources/Hummingbird/Files/LocalFileSystem.swift +++ b/Sources/Hummingbird/Files/LocalFileSystem.swift @@ -89,16 +89,16 @@ public struct LocalFileSystem: FileProvider { /// - Returns: File attributes public func getAttributes(id path: FileIdentifier) async throws -> FileAttributes? { do { - let lstat = try await self.fileIO.fileIO.lstat(path: path) - let isFolder = (lstat.st_mode & S_IFMT) == S_IFDIR + let stat = try await self.fileIO.fileIO.stat(path: path) + let isFolder = (stat.st_mode & S_IFMT) == S_IFDIR #if os(Linux) - let modificationDate = Double(lstat.st_mtim.tv_sec) + (Double(lstat.st_mtim.tv_nsec) / 1_000_000_000.0) + let modificationDate = Double(stat.st_mtim.tv_sec) + (Double(stat.st_mtim.tv_nsec) / 1_000_000_000.0) #else - let modificationDate = Double(lstat.st_mtimespec.tv_sec) + (Double(lstat.st_mtimespec.tv_nsec) / 1_000_000_000.0) + let modificationDate = Double(stat.st_mtimespec.tv_sec) + (Double(stat.st_mtimespec.tv_nsec) / 1_000_000_000.0) #endif return .init( isFolder: isFolder, - size: numericCast(lstat.st_size), + size: numericCast(stat.st_size), modificationDate: Date(timeIntervalSince1970: modificationDate) ) } catch { diff --git a/Tests/HummingbirdTests/FileMiddlewareTests.swift b/Tests/HummingbirdTests/FileMiddlewareTests.swift index 7ff9cf02..3d5aa906 100644 --- a/Tests/HummingbirdTests/FileMiddlewareTests.swift +++ b/Tests/HummingbirdTests/FileMiddlewareTests.swift @@ -16,6 +16,7 @@ import Foundation import HTTPTypes import Hummingbird import HummingbirdTesting +import NIOPosix import XCTest final class FileMiddlewareTests: XCTestCase { @@ -317,6 +318,43 @@ final class FileMiddlewareTests: XCTestCase { } } + func testSymlink() async throws { + let router = Router() + router.middlewares.add(FileMiddleware(".", searchForIndexHtml: true)) + let app = Application(responder: router.buildResponder()) + + let text = "Test file contents" + let data = Data(text.utf8) + let fileURL = URL(fileURLWithPath: "test.html") + XCTAssertNoThrow(try data.write(to: fileURL)) + defer { XCTAssertNoThrow(try FileManager.default.removeItem(at: fileURL)) } + + let fileIO = NonBlockingFileIO(threadPool: .singleton) + + try await app.test(.router) { client in + try await client.execute(uri: "/test.html", method: .get) { response in + XCTAssertEqual(String(buffer: response.body), text) + } + + try await client.execute(uri: "/", method: .get) { response in + XCTAssertEqual(String(buffer: response.body), "") + } + + try await fileIO.symlink(path: "index.html", to: "test.html") + + do { + try await client.execute(uri: "/", method: .get) { response in + XCTAssertEqual(String(buffer: response.body), text) + } + + try await fileIO.unlink(path: "index.html") + } catch { + try await fileIO.unlink(path: "index.html") + throw error + } + } + } + func testOnThrowCustom404() async throws { let router = Router() router.middlewares.add(FileMiddleware(".", searchForIndexHtml: true))