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
7 changes: 4 additions & 3 deletions src/main/services/tableService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@ export function createTableService(): TableService {
};

return {
async open(path, exclusive) {
async open(path, _exclusive) {
// 'r+' so a later REPLACE can write; a table that is read-only on disk still opens to read,
// and says so, because a browser that quietly refuses every edit is worse than one that
// says the file cannot be written
let writable = exclusive;
const dbf = await open(path, exclusive ? 'r+' : 'r').catch(() => {
// Shared access is not read-only access. Both modes need a writable descriptor.
let writable = true;
const dbf = await open(path, 'r+').catch(() => {
writable = false;
return open(path, 'r');
});
Expand Down
50 changes: 50 additions & 0 deletions tests/main/tableWrites.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createTableService } from '@main/services/tableService';

let root: string;
let tables: ReturnType<typeof createTableService>;
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'foxdev-table-write-'));
tables = createTableService();
});
afterEach(async () => {
await tables.closeAll();
await rm(root, { recursive: true, force: true });
});

describe('writable table handles', () => {
it.each([false, true])('persists bytes when exclusive=%s', async (exclusive) => {
const path = join(root, 'sample.dbf');
// The service treats DBF contents as bytes; only header length is decoded.
const original = Buffer.alloc(40);
original.writeUInt16LE(32, 8);
original.writeUInt16LE(8, 10);
await writeFile(path, original);
const opened = await tables.open(path, exclusive);
await tables.write(opened.handle, 32, ' changed');
await tables.close(opened.handle);
const expected = Buffer.from(original);
expected.write(' changed', 32, 'latin1');
expect(await readFile(path)).toEqual(expected);
expect(opened.writable).toBe(true);
});
});

// POSIX mode bits provide a real read-only file; Windows and root need another fixture.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)('falls back to read-only when writing is denied', async () => {
const path = join(root, 'readonly.dbf');
const bytes = Buffer.alloc(32);
bytes.writeUInt16LE(32, 8);
await writeFile(path, bytes);
await chmod(path, 0o444);
try {
const opened = await tables.open(path, false);
expect(opened.writable).toBe(false);
expect(await tables.read(opened.handle, 0, 32)).toBe(bytes.toString('latin1'));
} finally {
await chmod(path, 0o644);
}
});
Loading