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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ archives are vendored under `vendor/zennotes/` with their source identity and
checksums (`manifest.json`), and `package-lock.json` pins the complete install.
A clean checkout installs them with `npm ci`, without a source clone or sibling
repository. The vendored set is the core release
[core-2.60.0-core.hb0d0b54f320a8e3f](https://github.com/ZenNotes/zennotes/releases/tag/core-2.60.0-core.hb0d0b54f320a8e3f)
(desktop commit `15829394`, clean tree).
[core-2.60.1-core.h05ebb55c14afffb2](https://github.com/ZenNotes/zennotes/releases/tag/core-2.60.1-core.h05ebb55c14afffb2)
(desktop commit `4c74b478`, clean tree).

`npm run boundaries:check` verifies the pins, installed versions, singleton
React/CodeMirror peers, and public export usage. `npm run core:adopt -- <core-tag>`
Expand Down
8 changes: 8 additions & 0 deletions ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@
B1F519EA73BD94F20A3F1DD1 /* ICloudVaultPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA3D1BD7D10EE9B188D5FBF /* ICloudVaultPlugin.swift */; };
B516881658A9D3C9AFC1259E /* RecentNotesWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81BE1DA68F6223E14C0761AA /* RecentNotesWidget.swift */; };
BA7E23B65E109265261E0F5E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 155E04D763AD80DA2E56D38D /* Foundation.framework */; };
C10DF11E5A2B4C7D9E8F0A1B /* CloudFilesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10DF11F5A2B4C7D9E8F0A1C /* CloudFilesPlugin.swift */; };
D17E4E8551A97CF08FDE5F82 /* FolderPickerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A91962841901607F2906263 /* FolderPickerPlugin.swift */; };
DE19FCF1249C6A4D528227C2 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E1424FEA5946506EA505941C /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F59E347AA2D96EB117D7C2CD /* ZenWidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C218F6CD95CD249E2926D1DA /* ZenWidgetsBundle.swift */; };
FB56F003004840A2A553E931 /* KeyboardBackdropPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424F5A2D64394F429AF0ED50 /* KeyboardBackdropPlugin.swift */; };
FD65529207121AA61FF892C5 /* CloudFileStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9115A46C405EC261610EC467 /* CloudFileStream.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -112,10 +114,12 @@
85847153B31EF561562A9281 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/SwiftUI.framework; sourceTree = DEVELOPER_DIR; };
8A91962841901607F2906263 /* FolderPickerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FolderPickerPlugin.swift; sourceTree = "<group>"; };
8EAB64B0086A91CBCB3B5802 /* WidgetSnapshot.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WidgetSnapshot.swift; sourceTree = "<group>"; };
9115A46C405EC261610EC467 /* CloudFileStream.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFileStream.swift; sourceTree = "<group>"; };
A24C226FEA7EA30D28C20477 /* ZNViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ZNViewController.swift; sourceTree = "<group>"; };
A24C2270EA7EA30D28C20477 /* SceneDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
C10DF11F5A2B4C7D9E8F0A1C /* CloudFilesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFilesPlugin.swift; sourceTree = "<group>"; };
C218F6CD95CD249E2926D1DA /* ZenWidgetsBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ZenWidgetsBundle.swift; sourceTree = "<group>"; };
CC30A5CCEA5991955FBC0360 /* CloudFlowUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudFlowUITests.swift; sourceTree = "<group>"; };
CC30A5CDEA5991955FBC0360 /* DeepLinkUITests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeepLinkUITests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -223,6 +227,8 @@
8A91962841901607F2906263 /* FolderPickerPlugin.swift */,
D61B685B388CE720F2588065 /* PrivacyInfo.xcprivacy */,
830C64B43794EBCBD9091276 /* WidgetBridgePlugin.swift */,
C10DF11F5A2B4C7D9E8F0A1C /* CloudFilesPlugin.swift */,
9115A46C405EC261610EC467 /* CloudFileStream.swift */,
);
path = App;
sourceTree = "<group>";
Expand Down Expand Up @@ -515,6 +521,8 @@
B1F519EA73BD94F20A3F1DD1 /* ICloudVaultPlugin.swift in Sources */,
D17E4E8551A97CF08FDE5F82 /* FolderPickerPlugin.swift in Sources */,
66714DDFC6EB0A9732DE176A /* WidgetBridgePlugin.swift in Sources */,
C10DF11E5A2B4C7D9E8F0A1B /* CloudFilesPlugin.swift in Sources */,
FD65529207121AA61FF892C5 /* CloudFileStream.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
144 changes: 144 additions & 0 deletions ios/App/App/CloudFileStream.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import CryptoKit
import Foundation

/// File integrity checks shared by the native plugin and host-side regression tests.
enum CloudFileStream {
static let bufferSize = 64 * 1024
static let inlineLimit = 5 * 1024 * 1024
struct Fingerprint { let byteLength: Int; let sha256: String; let utf8: Bool; let inline: Data? }
struct Expectation { let byteLength: Int; let sha256: String }
enum Failure: Error { case unreadable, lengthMismatch, hashMismatch, overflow }

static func fingerprint(_ url: URL, textCandidate: Bool) throws -> Fingerprint {
guard let input = InputStream(url: url) else { throw Failure.unreadable }
input.open(); defer { input.close() }
guard input.streamStatus != .error else { throw input.streamError ?? Failure.unreadable }
var hasher = SHA256()
var length = 0
var inline: Data? = Data()
var utf8Check = textCandidate ? Utf8Validator() : nil
var buffer = [UInt8](repeating: 0, count: bufferSize)
while true {
let count = input.read(&buffer, maxLength: buffer.count)
if count < 0 { throw input.streamError ?? Failure.unreadable }
if count == 0 { break }
length += count
buffer.withUnsafeBufferPointer { hasher.update(bufferPointer: UnsafeRawBufferPointer(UnsafeBufferPointer(rebasing: $0.prefix(count)))) }
if inline != nil {
if length <= inlineLimit { inline!.append(buffer, count: count) } else { inline = nil }
}
utf8Check?.accept(buffer.prefix(count))
}
let digest = hasher.finalize().map { String(format: "%02x", $0) }.joined()
return Fingerprint(byteLength: length, sha256: digest, utf8: utf8Check?.finish() ?? false, inline: inline)
}

static func copyVerified(_ input: InputStream, to destination: URL, expected: Expectation) throws {
try FileManager.default.createDirectory(at: destination.deletingLastPathComponent(), withIntermediateDirectories: true)
input.open()
defer { input.close() }
guard input.streamStatus != .error else { throw input.streamError ?? Failure.unreadable }
guard let output = OutputStream(url: destination, append: false) else { throw Failure.unreadable }
output.open()
defer { output.close() }
guard output.streamStatus != .error else { throw output.streamError ?? Failure.unreadable }
var hasher = SHA256()
var written = 0
var buffer = [UInt8](repeating: 0, count: bufferSize)
while true {
let count = input.read(&buffer, maxLength: buffer.count)
if count < 0 { throw input.streamError ?? Failure.unreadable }
if count == 0 { break }
written += count
if written > expected.byteLength { throw Failure.overflow }
buffer.withUnsafeBufferPointer { hasher.update(bufferPointer: UnsafeRawBufferPointer(UnsafeBufferPointer(rebasing: $0.prefix(count)))) }
var offset = 0
while offset < count {
let out = buffer.withUnsafeBufferPointer {
output.write($0.baseAddress!.advanced(by: offset), maxLength: count - offset)
}
if out <= 0 { throw output.streamError ?? Failure.unreadable }
offset += out
}
}
if written != expected.byteLength { throw Failure.lengthMismatch }
let digest = hasher.finalize().map { String(format: "%02x", $0) }.joined()
if digest != expected.sha256 { throw Failure.hashMismatch }
}

static func confinedURL(_ value: String, roots: [URL]) -> URL? {
guard let url = URL(string: value), url.isFileURL else { return nil }
guard !url.pathComponents.contains(".."), let resolved = canonicalDestination(url) else { return nil }
let path = resolved.path
return roots.contains { root in
let rootPath = root.resolvingSymlinksInPath().standardizedFileURL.path
return path.hasPrefix(rootPath.hasSuffix("/") ? rootPath : rootPath + "/")
} ? resolved : nil
}

static func allowedHeader(_ name: String, value: String, url: URL) -> Bool {
guard name.range(of: "^[A-Za-z0-9-]+$", options: .regularExpression) != nil,
name.rangeOfCharacter(from: .newlines) == nil,
value.rangeOfCharacter(from: .newlines) == nil else { return false }
if name.lowercased() == "host" {
guard let host = url.host else { return false }
let authority = host + (url.port.map { ":\($0)" } ?? "")
return value.lowercased() == authority.lowercased()
}
return !["authorization", "proxy-authorization", "cookie", "connection", "transfer-encoding"].contains(name.lowercased())
}

private static func canonicalDestination(_ url: URL) -> URL? {
var ancestor = url.standardizedFileURL
var missing: [String] = []
let fm = FileManager.default
// Foundation does not resolve a symlinked parent when the leaf does
// not exist. Resolve the nearest existing ancestor before adding it.
while !fm.fileExists(atPath: ancestor.path) {
if (try? fm.attributesOfItem(atPath: ancestor.path)[.type]) as? FileAttributeType == .typeSymbolicLink {
return nil
}
guard ancestor.path != "/" else { return nil }
missing.append(ancestor.lastPathComponent)
ancestor.deleteLastPathComponent()
}
var resolved = ancestor.resolvingSymlinksInPath().standardizedFileURL
for component in missing.reversed() { resolved.appendPathComponent(component) }
return resolved
}
}

/// Incremental UTF-8 validation across chunk boundaries.
struct Utf8Validator {
private var remaining = 0
private var lower: UInt8 = 0x80
private var upper: UInt8 = 0xBF
private var valid = true

mutating func accept(_ chunk: ArraySlice<UInt8>) {
guard valid else { return }
for byte in chunk {
if remaining > 0 {
guard byte >= lower && byte <= upper else { valid = false; return }
remaining -= 1
lower = 0x80; upper = 0xBF
} else {
switch byte {
case 0...0x7F: break
case 0xC2...0xDF: remaining = 1
case 0xE0: remaining = 2; lower = 0xA0
case 0xE1...0xEC, 0xEE...0xEF: remaining = 2
case 0xED: remaining = 2; upper = 0x9F
case 0xF0: remaining = 3; lower = 0x90
case 0xF1...0xF3: remaining = 3
case 0xF4: remaining = 3; upper = 0x8F
default: valid = false; return
}
}
}
}

mutating func finish() -> Bool {
valid && remaining == 0
}
}
Loading
Loading