Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Packages/PeerCore/Sources/PeerCore/InfoBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ public enum InfoBase {
throw PeerError.connection("信息库未返回资料标识。")
}

public static func updateBlock(_ id: Int, content: String, on connection: PostgresConnection)
public static func updateBlock(
_ id: Int, content: String, resolver: String? = nil, on connection: PostgresConnection
)
async throws
{
let rows = try await connection.query(
"UPDATE inkcre.blocks SET content=\(content) WHERE id=\(id) RETURNING id",
"UPDATE inkcre.blocks SET content=\(content),resolver=COALESCE(\(resolver),resolver) WHERE id=\(id) RETURNING id",
logger: PeerDatabase.logger)
for try await _ in rows.decode(Int.self) { return }
throw PeerError.needsAction("资料已被移除。")
Expand Down
6 changes: 5 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ PostgresNIO 及传递依赖使用各自许可。Package.resolved 固定来源与

InKCre UI 的 `InKCreLogoAssets` 用于本项目品牌;代码 MIT 许可不授予 InKCre 商标或品牌资源的使用权。打包从锁定的 UI checkout 收集 `assets/logo/README.zh-CN.md` 与 `PROVENANCE.json`,保留资源来源和使用边界;任一文件缺失仍使打包失败。其他依赖继续要求完整许可。应用图标的历史来源另保留于本机品牌目录。

[SwiftSoup 2.14.3](https://github.com/scinfu/SwiftSoup/tree/2.14.3)(e7f85f4d3dd91ca9a1e94d3328aa1ed5120a8c0f)用于本地 HTML 文字解析,使用 MIT。构建收集上游完整 LICENSE,保留原 jsoup 作者与 Swift port 作者署名。
[SwiftSoup 2.14.3](https://github.com/scinfu/SwiftSoup/tree/2.14.3)(e7f85f4d3dd91ca9a1e94d3328aa1ed5120a8c0f)用于本地 HTML 结构解析与安全清理,使用 MIT。构建收集上游完整 LICENSE,保留原 jsoup 作者与 Swift port 作者署名。

[Hister](https://github.com/asciimoo/hister) 的目录监视与客户端提取行为为本地文件功能提供启发。参考版本 f1fff29683c8c8006a35d3387384d9b1e123cebe,项目使用 AGPL-3.0-or-later。本实现未复制或改写其源码,不运行其服务;本致谢不表示上游参与或背书。

[swift-readability](https://github.com/neolee/swift-readability/tree/53be6c0d9c3723a7f9acb17324d435740347a872) 使用 MIT,固定修订用于已取得 HTML 的正文提取。[swift-cmark 0.9.0](https://github.com/swiftlang/swift-cmark/tree/0.9.0) 用于临时 GFM 阅读转换。打包从锁定源码收集完整 LICENSE/COPYING,包含各自上游作者声明。

本轮页面内容与结构提取同时参考 Asciimoo 及 Hister 贡献者的工作,固定修订为 `2e017c05cf544b812b8fd509ff3fc07c5c7e464e`。实现保留已取得结构及已有来源事实,不复制、改写或运行 Hister 源码,不采用其全部内容删减策略。
115 changes: 103 additions & 12 deletions Tests/LocalFilesTests/LocalFilesInterfaceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import PeerCore
import Security
import SwiftUI
import Testing
import WebKit

@testable import LocalFilesMac

Expand Down Expand Up @@ -58,8 +59,19 @@ struct LocalFilesInterfaceTests {
let file = try #require(model.records.first { $0.name == "note.md" })
model.selectedFile = model.records.first { $0.name == "note.pdf" }?.id
model.selectedFile = file.id
#expect(model.selectedText == nil && model.loadingText)
try await until { model.selectedText == "# 标题\n\n第一段。\n\n- 列表\n" && !model.loadingText }
#expect(model.selectedContent == nil && model.loadingContent)
try await until {
model.selectedContent == "# 标题\n\n第一段。\n\n- 列表\n" && !model.loadingContent
}
try await until {
guard let view = window.contentView, let reader = self.webReader(in: view),
!reader.isLoading
else { return false }
return
(try? await reader.evaluateJavaScript("document.querySelector('h1')?.textContent"))
as? String == "标题"
}
#expect(!window.isVisible)
let database = PeerDatabase.shared
let pool = try await database.client(
for: target, password: try #require(env["INKCRE_FILES_TEST_PASSWORD"]))
Expand All @@ -73,35 +85,67 @@ struct LocalFilesInterfaceTests {
)
.decode(String.self).first { @Sendable _ in true }
#expect(catalogVersion == afterRefresh)
#expect(model.selectedText != nil && !model.loadingText)
#expect(model.selectedContent != nil && !model.loadingContent)
try await snapshot(window, name: "files-body")
model.selectedFile = model.records.first { $0.name == "note.html" }?.id
try await until {
guard model.file?.bodyResolver == "core.html.v1", !model.loadingContent,
let view = window.contentView, let reader = self.webReader(in: view),
!reader.isLoading
else { return false }
return
(try? await reader.evaluateJavaScript("document.querySelector('td')?.textContent"))
as? String == "A"
}
let reader = try #require(window.contentView.flatMap(webReader))
let code = try await reader.evaluateJavaScript("document.querySelector('pre')?.textContent")
#expect((code as? String)?.contains("<node>& value\n indented") == true)
#expect(
try await reader.evaluateJavaScript("typeof window.not_body") as? String == "undefined")
#expect(
try await reader.evaluateJavaScript(
"document.querySelectorAll('video,track,[src]').length") as? Int == 0)
#expect(
try await reader.evaluateJavaScript(
"!!document.querySelector('a[href=\"https://example.org/captions.vtt\"]')") as? Bool
== true)
try await snapshot(window, name: "files-html")
if let path = env["INKCRE_LAYOUT_EVIDENCE_DIR"] {
let image = try await reader.takeSnapshot(configuration: nil)
let data = try #require(image.tiffRepresentation)
try #require(NSBitmapImageRep(data: data)?.representation(using: .png, properties: [:]))
.write(
to: URL(fileURLWithPath: path).appendingPathComponent("files-html-content.png"))
}
model.selectedFile = file.id
try await until { model.selectedContent == "# 标题\n\n第一段。\n\n- 列表\n" }
let url = folder.appendingPathComponent("note.md")
try Data("# 自动监视后的正文".utf8).write(to: url, options: .atomic)
try await until { model.selectedText == "# 自动监视后的正文" }
try await until { model.selectedContent == "# 自动监视后的正文" }
let source = try #require(model.source)
model.toggle(source)
try await until { model.source?.config.enabled == false && !model.working }
try Data("# 暂停时修改".utf8).write(to: url, options: .atomic)
try await Task.sleep(for: .seconds(2))
#expect(model.selectedText == "# 自动监视后的正文")
#expect(model.selectedContent == "# 自动监视后的正文")
try await snapshot(window, name: "files-paused")
model.toggle(try #require(model.source))
try await until { model.selectedText == "# 暂停时修改" && !model.working }
try await until { model.selectedContent == "# 暂停时修改" && !model.working }
await model.stop()
try Data("# 停机时修改".utf8).write(to: url, options: .atomic)
try model.start()
try await until { model.selectedText == "# 停机时修改" }
try await until { model.selectedContent == "# 停机时修改" }
try FileManager.default.removeItem(at: url)
try await until { model.file?.metadata.available == false }
#expect(model.selectedText == "# 停机时修改")
#expect(model.selectedContent == "# 停机时修改")
try await snapshot(window, name: "files-original-missing")
try Data("not a pdf".utf8).write(
to: folder.appendingPathComponent("note.pdf"), options: .atomic)
try await until {
model.records.contains { $0.name == "note.pdf" && $0.metadata.issue != nil }
}
model.selectedFile = model.records.first { $0.name == "note.pdf" }?.id
try await until { model.selectedText != nil && !model.loadingText }
try await until { model.selectedContent != nil && !model.loadingContent }
try await snapshot(window, name: "files-failure")
let restoredSource = try #require(model.source)
let client = LocalFileClient(
Expand All @@ -113,15 +157,21 @@ struct LocalFilesInterfaceTests {
await model.stop()
}

private func until(_ condition: @escaping @MainActor () -> Bool) async throws {
private func until(_ condition: @escaping @MainActor () async -> Bool) async throws {
for _ in 0..<150 {
if condition() { return }
if await condition() { return }
try await Task.sleep(for: .milliseconds(100))
}
#expect(condition(), "等待本地文件主流程状态超时")
#expect(await condition(), "等待本地文件主流程状态超时")
throw PeerError.needsAction("测试等待状态超时。")
}

private func webReader(in view: NSView) -> WKWebView? {
if let reader = view as? WKWebView { return reader }
for child in view.subviews { if let reader = webReader(in: child) { return reader } }
return nil
}

private func snapshot(_ window: NSWindow, name: String) async throws {
for appearance in [NSAppearance.Name.aqua, .darkAqua] {
window.appearance = NSAppearance(named: appearance)
Expand All @@ -139,3 +189,44 @@ struct LocalFilesInterfaceTests {
}
}
}

@Test func safeFileReadingKeepsStructureAndBlocksSourceCode() throws {
let source = """
# 标题

| Item | Value |
| --- | --- |
| A | 12 |

- [x] 完成
- [ ] 待办

```html
<node>& value
indented
```

~~删除~~ https://example.org

<a href="#part">文内目录</a><h2 id="part">章节</h2>

<script>window.executed = true</script><iframe src="https://invalid.test/frame"></iframe>
<img src="https://example.org/image.png" alt="图片说明"><a href="javascript:alert(1)">bad</a>
<video src="https://example.org/video.mp4"><track src="https://example.org/captions.vtt" label="English"><p>已有文字稿 <a href="https://example.org/transcript">完整文字稿</a></p></video>
"""
let html = try readingDocument(source, resolver: "core.markdown.v1")
#expect(html.contains("<table>") && html.contains("<del>删除</del>"))
#expect(html.contains("&lt;node&gt;&amp; value\n indented"))
#expect(html.contains("checked") && html.contains("disabled"))
#expect(html.contains("https://example.org/image.png") && html.contains("图片说明"))
#expect(
!html.contains("window.executed") && !html.contains("<iframe")
&& !html.contains("javascript:"))
#expect(
html.contains("https://example.org/video.mp4")
&& html.contains("https://example.org/captions.vtt"))
#expect(!html.contains("<video") && !html.contains("<track"))
#expect(html.contains("已有文字稿") && html.contains("https://example.org/transcript"))
#expect(html.contains("default-src 'none'"))
#expect(html.contains("href=\"#part\"") && html.contains("id=\"part\""))
}
Loading
Loading