From 8216b7a0867de382c97f2539507f55007eccd676 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 12 Jul 2026 08:27:08 +0000 Subject: [PATCH 1/2] fix: preserve external Buffer view bounds --- src/StorageAccessor/DirectVault.ts | 8 +++- .../ExternalVaultFilesystem.test.ts | 36 ++++++++++++++++++ .../ExternalVaultFilesystem.ts | 9 +++-- src/StorageAccessor/NormalVault.ts | 8 +++- src/StorageAccessor/S3Bucket.ts | 10 +++-- src/StorageAccessor/StorageAccessor.ts | 20 ++++++---- src/StorageAccessor/storage-contracts.ts | 26 +++++++++++++ src/storage.ts | 37 +++++-------------- src/util.test.ts | 1 + 9 files changed, 109 insertions(+), 46 deletions(-) create mode 100644 src/StorageAccessor/ExternalVaultFilesystem.test.ts create mode 100644 src/StorageAccessor/storage-contracts.ts diff --git a/src/StorageAccessor/DirectVault.ts b/src/StorageAccessor/DirectVault.ts index d3e67ec..daa0a44 100644 --- a/src/StorageAccessor/DirectVault.ts +++ b/src/StorageAccessor/DirectVault.ts @@ -1,6 +1,6 @@ -import type { Stat } from "obsidian"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; +import { normalizePath, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; export class DirectVault extends StorageAccessor { @@ -8,6 +8,10 @@ export class DirectVault extends StorageAccessor { sep = "/"; // Always use / as separator on vault. + normalizePath(path: string): string { + return normalizePath(path); + } + async createFolder(absolutePath: string): Promise { await this.app.vault.adapter.mkdir(absolutePath); } diff --git a/src/StorageAccessor/ExternalVaultFilesystem.test.ts b/src/StorageAccessor/ExternalVaultFilesystem.test.ts new file mode 100644 index 0000000..f6215bb --- /dev/null +++ b/src/StorageAccessor/ExternalVaultFilesystem.test.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line import/no-nodejs-modules -- Electron file reads return Node Buffers. +import { Buffer } from "node:buffer"; +import { ExternalVaultFilesystem } from "./ExternalVaultFilesystem.ts"; +import type { StorageAccessorHost } from "./storage-contracts.ts"; + +declare const Deno: { + test: (name: string, fn: () => void | Promise) => void; +}; + +function assertEquals(actual: T, expected: T, message: string) { + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + throw new Error(`${message}\nactual=${JSON.stringify(actual)}\nexpected=${JSON.stringify(expected)}`); + } +} + +Deno.test("ExternalVaultFilesystem: reads only the visible Node Buffer bytes", async () => { + const backing = Buffer.from([99, 1, 2, 3, 88]); + const visible = backing.subarray(1, 4); + const plugin = { + app: { + vault: { + adapter: { + fsPromises: { + readFile: async () => visible, + }, + }, + }, + }, + } as unknown as StorageAccessorHost; + const storage = new ExternalVaultFilesystem(plugin); + + const result = await storage._readBinary("archive.bin"); + if (result === false) throw new Error("Expected binary data"); + + assertEquals([...new Uint8Array(result)], [1, 2, 3], "Buffer byte offset and length should be preserved"); +}); diff --git a/src/StorageAccessor/ExternalVaultFilesystem.ts b/src/StorageAccessor/ExternalVaultFilesystem.ts index cfed4f5..f99d8f7 100644 --- a/src/StorageAccessor/ExternalVaultFilesystem.ts +++ b/src/StorageAccessor/ExternalVaultFilesystem.ts @@ -1,8 +1,11 @@ import type { Stat } from "obsidian"; -import { type FsAPI, FileType, StorageAccessorTypes } from "../storage.ts"; +// eslint-disable-next-line import/no-nodejs-modules -- Electron exposes the Node filesystem API used here. +import type { promises } from "node:fs"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; import { toArrayBuffer } from "../util.ts"; +type FsAPI = Pick; export class ExternalVaultFilesystem extends StorageAccessor { type = StorageAccessorTypes.EXTERNAL; @@ -21,7 +24,7 @@ export class ExternalVaultFilesystem extends StorageAccessor { await this.fsPromises.mkdir(absolutePath, { recursive: true }); } - async ensureDirectory(fullPath: string) { + override async ensureDirectory(fullPath: string) { const delimiter = this.sep; const pathElements = fullPath.split(delimiter); pathElements.pop(); @@ -42,7 +45,7 @@ export class ExternalVaultFilesystem extends StorageAccessor { async _readBinary(path: string): Promise { const buffer = await this.fsPromises.readFile(path); - return toArrayBuffer(buffer.buffer) + return toArrayBuffer(buffer); } async deleteBinary(path: string): Promise { diff --git a/src/StorageAccessor/NormalVault.ts b/src/StorageAccessor/NormalVault.ts index 7298b42..636a057 100644 --- a/src/StorageAccessor/NormalVault.ts +++ b/src/StorageAccessor/NormalVault.ts @@ -1,6 +1,6 @@ -import { TFile, TFolder, type Stat } from "obsidian"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; +import { normalizePath, TFile, TFolder, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; export class NormalVault extends StorageAccessor { @@ -8,6 +8,10 @@ export class NormalVault extends StorageAccessor { sep = "/"; // Always use / as separator on vault. + normalizePath(path: string): string { + return normalizePath(path); + } + async createFolder(absolutePath: string): Promise { await this.app.vault.createFolder(absolutePath); } diff --git a/src/StorageAccessor/S3Bucket.ts b/src/StorageAccessor/S3Bucket.ts index 2c40f3e..521102f 100644 --- a/src/StorageAccessor/S3Bucket.ts +++ b/src/StorageAccessor/S3Bucket.ts @@ -1,8 +1,8 @@ import { S3 } from "@aws-sdk/client-s3"; -import type { Stat } from "obsidian"; +import { normalizePath, type Stat } from "obsidian"; import { ObsHttpHandler } from "../ObsHttpHandler.ts"; -import { FileType, StorageAccessorTypes } from "../storage.ts"; import { StorageAccessor } from "./StorageAccessor.ts"; +import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; import { toArrayBuffer } from "../util.ts"; @@ -10,11 +10,15 @@ export class S3Bucket extends StorageAccessor { type = StorageAccessorTypes.S3; sep = "/"; + normalizePath(path: string): string { + return normalizePath(path); + } + createFolder(absolutePath: string): Promise { // S3 does not have folder concept. So, we don't need to create folder. return Promise.resolve(); } - ensureDirectory(fullPath: string): Promise { + override ensureDirectory(fullPath: string): Promise { return Promise.resolve(); } diff --git a/src/StorageAccessor/StorageAccessor.ts b/src/StorageAccessor/StorageAccessor.ts index a0619ae..a86a207 100644 --- a/src/StorageAccessor/StorageAccessor.ts +++ b/src/StorageAccessor/StorageAccessor.ts @@ -1,13 +1,19 @@ -import { normalizePath, type Stat } from "obsidian"; -import type DiffZipBackupPlugin from "../../main.ts"; -import { type StorageAccessorType, FileType, decryptCompatOpenSSL, encryptCompatOpenSSL } from "../storage.ts"; +import type { Stat } from "obsidian"; +import { OpenSSLCompat } from "octagonal-wheels/encryption"; +import { + type StorageAccessorHost, + type StorageAccessorType, + FileType, +} from "./storage-contracts.ts"; import { toArrayBuffer } from "../util.ts"; +const decryptCompatOpenSSL = OpenSSLCompat.CBC.decryptCBC; +const encryptCompatOpenSSL = OpenSSLCompat.CBC.encryptCBC; export abstract class StorageAccessor { abstract type: StorageAccessorType; abstract sep: string; - public plugin: DiffZipBackupPlugin; + public plugin: StorageAccessorHost; get app() { return this.plugin.app; } @@ -21,7 +27,7 @@ export abstract class StorageAccessor { } public isLocal: boolean = false; - constructor(plugin: DiffZipBackupPlugin, basePath?: string, isLocal?: boolean) { + constructor(plugin: StorageAccessorHost, basePath?: string, isLocal?: boolean) { this.basePath = basePath || ""; this.plugin = plugin; this.isLocal = isLocal || false; @@ -73,9 +79,7 @@ export abstract class StorageAccessor { abstract _readBinary(path: string, preventUseCache?: boolean): Promise; abstract deleteBinary(path: string): Promise; - normalizePath(path: string): string { - return normalizePath(path); - } + abstract normalizePath(path: string): string; abstract stat(path: string): Promise; async ensureDirectory(fullPath: string) { diff --git a/src/StorageAccessor/storage-contracts.ts b/src/StorageAccessor/storage-contracts.ts new file mode 100644 index 0000000..ce4a259 --- /dev/null +++ b/src/StorageAccessor/storage-contracts.ts @@ -0,0 +1,26 @@ +import type { App } from "obsidian"; +import type { DiffZipBackupSettings } from "../types.ts"; + +/** Minimal plug-in boundary required by storage accessor implementations. */ +export interface StorageAccessorHost { + readonly app: App; + readonly settings: DiffZipBackupSettings; +} + +/** File kind reported by a storage accessor. */ +export enum FileType { + Missing, + File, + Folder, +} + +/** Stable storage accessor identifiers. */ +export const StorageAccessorTypes = { + NORMAL: "normal", + DIRECT: "direct", + EXTERNAL: "external", + S3: "s3", +} as const; + +/** Identifier for one DiffZip storage accessor implementation. */ +export type StorageAccessorType = typeof StorageAccessorTypes[keyof typeof StorageAccessorTypes]; diff --git a/src/storage.ts b/src/storage.ts index ba3a7ce..88f3f62 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -2,40 +2,21 @@ * Abstract class for storage accessors and its implementations. */ import type DiffZipBackupPlugin from "../main.ts"; -// Because of this is a type-only import. -// eslint-disable-next-line import/no-nodejs-modules -- type only import of promises -import type { promises } from "node:fs"; -import { OpenSSLCompat } from "octagonal-wheels/encryption"; import { NormalVault } from "./StorageAccessor/NormalVault.ts"; import { DirectVault } from "./StorageAccessor/DirectVault.ts"; import { ExternalVaultFilesystem } from "./StorageAccessor/ExternalVaultFilesystem.ts"; import { S3Bucket } from "./StorageAccessor/S3Bucket.ts"; import type { StorageAccessor } from "./StorageAccessor/StorageAccessor.ts"; -export const decryptCompatOpenSSL = OpenSSLCompat.CBC.decryptCBC; -export const encryptCompatOpenSSL = OpenSSLCompat.CBC.encryptCBC; +import { + StorageAccessorTypes, + type StorageAccessorType, +} from "./StorageAccessor/storage-contracts.ts"; -export enum FileType { - "Missing", - "File", - "Folder", -} - -export type FsAPI = { - mkdir: typeof promises.mkdir; - writeFile: typeof promises.writeFile; - readFile: typeof promises.readFile; - stat: typeof promises.stat; - rm: typeof promises.rm; -}; - -export const StorageAccessorTypes = { - NORMAL: "normal", - DIRECT: "direct", - EXTERNAL: "external", - S3: "s3", -} as const; - -export type StorageAccessorType = typeof StorageAccessorTypes[keyof typeof StorageAccessorTypes]; +export { + FileType, + StorageAccessorTypes, + type StorageAccessorType, +} from "./StorageAccessor/storage-contracts.ts"; export function getStorageTypeForBackupAccess(plugin: DiffZipBackupPlugin): StorageAccessorType { if (plugin.isDesktopMode) { diff --git a/src/util.test.ts b/src/util.test.ts index c9d9b6f..3561f21 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -33,6 +33,7 @@ Deno.test("toArrayBuffer: copies exactly the bytes visible through sliced views" const viewResult = toArrayBuffer(new DataView(bytes.buffer, 2, 2)); assertEquals([...new Uint8Array(viewResult)], [2, 3], "DataView bounds should be preserved"); assert(viewResult !== bytes.buffer, "a partial DataView should not expose its complete backing buffer"); + }); Deno.test("humanReadableSize: formats edge cases and byte units", () => { From 10e3fdf299a4c21bc722f1a9b011af31f0ea11b7 Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Sun, 12 Jul 2026 09:40:14 +0000 Subject: [PATCH 2/2] Clarify internal storage accessor types --- src/StorageAccessor/DirectVault.ts | 2 +- src/StorageAccessor/ExternalVaultFilesystem.test.ts | 2 +- src/StorageAccessor/ExternalVaultFilesystem.ts | 2 +- src/StorageAccessor/NormalVault.ts | 2 +- src/StorageAccessor/S3Bucket.ts | 2 +- src/StorageAccessor/StorageAccessor.ts | 2 +- .../{storage-contracts.ts => storage-accessor-types.ts} | 6 +++++- src/storage.ts | 4 ++-- src/util.test.ts | 1 - 9 files changed, 13 insertions(+), 10 deletions(-) rename src/StorageAccessor/{storage-contracts.ts => storage-accessor-types.ts} (80%) diff --git a/src/StorageAccessor/DirectVault.ts b/src/StorageAccessor/DirectVault.ts index daa0a44..9ee770a 100644 --- a/src/StorageAccessor/DirectVault.ts +++ b/src/StorageAccessor/DirectVault.ts @@ -1,6 +1,6 @@ import { normalizePath, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; -import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; export class DirectVault extends StorageAccessor { diff --git a/src/StorageAccessor/ExternalVaultFilesystem.test.ts b/src/StorageAccessor/ExternalVaultFilesystem.test.ts index f6215bb..3617068 100644 --- a/src/StorageAccessor/ExternalVaultFilesystem.test.ts +++ b/src/StorageAccessor/ExternalVaultFilesystem.test.ts @@ -1,7 +1,7 @@ // eslint-disable-next-line import/no-nodejs-modules -- Electron file reads return Node Buffers. import { Buffer } from "node:buffer"; import { ExternalVaultFilesystem } from "./ExternalVaultFilesystem.ts"; -import type { StorageAccessorHost } from "./storage-contracts.ts"; +import type { StorageAccessorHost } from "./storage-accessor-types.ts"; declare const Deno: { test: (name: string, fn: () => void | Promise) => void; diff --git a/src/StorageAccessor/ExternalVaultFilesystem.ts b/src/StorageAccessor/ExternalVaultFilesystem.ts index f99d8f7..ad7b0b5 100644 --- a/src/StorageAccessor/ExternalVaultFilesystem.ts +++ b/src/StorageAccessor/ExternalVaultFilesystem.ts @@ -2,7 +2,7 @@ import type { Stat } from "obsidian"; // eslint-disable-next-line import/no-nodejs-modules -- Electron exposes the Node filesystem API used here. import type { promises } from "node:fs"; import { StorageAccessor } from "./StorageAccessor.ts"; -import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; type FsAPI = Pick; diff --git a/src/StorageAccessor/NormalVault.ts b/src/StorageAccessor/NormalVault.ts index 636a057..a82dff1 100644 --- a/src/StorageAccessor/NormalVault.ts +++ b/src/StorageAccessor/NormalVault.ts @@ -1,6 +1,6 @@ import { normalizePath, TFile, TFolder, type Stat } from "obsidian"; import { StorageAccessor } from "./StorageAccessor.ts"; -import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; export class NormalVault extends StorageAccessor { diff --git a/src/StorageAccessor/S3Bucket.ts b/src/StorageAccessor/S3Bucket.ts index 521102f..63e4ea3 100644 --- a/src/StorageAccessor/S3Bucket.ts +++ b/src/StorageAccessor/S3Bucket.ts @@ -2,7 +2,7 @@ import { S3 } from "@aws-sdk/client-s3"; import { normalizePath, type Stat } from "obsidian"; import { ObsHttpHandler } from "../ObsHttpHandler.ts"; import { StorageAccessor } from "./StorageAccessor.ts"; -import { FileType, StorageAccessorTypes } from "./storage-contracts.ts"; +import { FileType, StorageAccessorTypes } from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; diff --git a/src/StorageAccessor/StorageAccessor.ts b/src/StorageAccessor/StorageAccessor.ts index a86a207..2fd4231 100644 --- a/src/StorageAccessor/StorageAccessor.ts +++ b/src/StorageAccessor/StorageAccessor.ts @@ -4,7 +4,7 @@ import { type StorageAccessorHost, type StorageAccessorType, FileType, -} from "./storage-contracts.ts"; +} from "./storage-accessor-types.ts"; import { toArrayBuffer } from "../util.ts"; const decryptCompatOpenSSL = OpenSSLCompat.CBC.decryptCBC; diff --git a/src/StorageAccessor/storage-contracts.ts b/src/StorageAccessor/storage-accessor-types.ts similarity index 80% rename from src/StorageAccessor/storage-contracts.ts rename to src/StorageAccessor/storage-accessor-types.ts index ce4a259..723e4ec 100644 --- a/src/StorageAccessor/storage-contracts.ts +++ b/src/StorageAccessor/storage-accessor-types.ts @@ -1,7 +1,11 @@ import type { App } from "obsidian"; import type { DiffZipBackupSettings } from "../types.ts"; -/** Minimal plug-in boundary required by storage accessor implementations. */ +/** + * DiffZip-owned context supplied to storage accessor implementations. + * + * This is an internal composition shape, not a platform-neutral storage contract. + */ export interface StorageAccessorHost { readonly app: App; readonly settings: DiffZipBackupSettings; diff --git a/src/storage.ts b/src/storage.ts index 88f3f62..9316d8d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -10,13 +10,13 @@ import type { StorageAccessor } from "./StorageAccessor/StorageAccessor.ts"; import { StorageAccessorTypes, type StorageAccessorType, -} from "./StorageAccessor/storage-contracts.ts"; +} from "./StorageAccessor/storage-accessor-types.ts"; export { FileType, StorageAccessorTypes, type StorageAccessorType, -} from "./StorageAccessor/storage-contracts.ts"; +} from "./StorageAccessor/storage-accessor-types.ts"; export function getStorageTypeForBackupAccess(plugin: DiffZipBackupPlugin): StorageAccessorType { if (plugin.isDesktopMode) { diff --git a/src/util.test.ts b/src/util.test.ts index 3561f21..c9d9b6f 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -33,7 +33,6 @@ Deno.test("toArrayBuffer: copies exactly the bytes visible through sliced views" const viewResult = toArrayBuffer(new DataView(bytes.buffer, 2, 2)); assertEquals([...new Uint8Array(viewResult)], [2, 3], "DataView bounds should be preserved"); assert(viewResult !== bytes.buffer, "a partial DataView should not expose its complete backing buffer"); - }); Deno.test("humanReadableSize: formats edge cases and byte units", () => {