fix: handle non-ISO-8859-1 characters in fetch headers and Content-Disposition (#1673)
* fix: handle non-ISO-8859-1 characters in fetch headers and Content-Disposition Browser Headers API rejects characters above U+00FF. The x-opencode-directory header carries raw filesystem paths, which breaks when paths contain Chinese/CJK characters. Also fixes Content-Disposition for non-ASCII filenames per RFC 5987. * refactor: export header sanitization helpers, deduplicate, add tests Export isLatin1Safe and sanitizeHeadersForBrowser from runtime-fetch.ts so VS Code webview can import them instead of duplicating the logic. Add tests: isLatin1Safe boundary checks, sanitizeHeadersForBrowser encoding/deduplication, runtimeFetch round-trip encode/decode, and Content-Disposition RFC 5987 output for both ASCII and non-ASCII filenames. * fix: mark encoded directory headers --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
43f677d56d
commit
efd621b087
@@ -1,5 +1,13 @@
|
||||
import { createRealpathCache } from '../path-realpath-cache.js';
|
||||
|
||||
// Browser transport percent-encodes directory hints and marks them explicitly.
|
||||
// Only marked values are decoded so literal percent sequences from direct API
|
||||
// clients are preserved.
|
||||
const safeDecodeMarkedURIComponent = (value, encoding) => {
|
||||
if (encoding !== 'uri') return value;
|
||||
try { return decodeURIComponent(value); } catch { return value; }
|
||||
};
|
||||
|
||||
export const createProjectDirectoryRuntime = (dependencies) => {
|
||||
const {
|
||||
fsPromises,
|
||||
@@ -50,7 +58,9 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
||||
};
|
||||
|
||||
const resolveProjectDirectory = async (req) => {
|
||||
const headerDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const rawHeaderDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const headerEncoding = typeof req.get === 'function' ? req.get('x-opencode-directory-encoding') : null;
|
||||
const headerDirectory = rawHeaderDirectory ? safeDecodeMarkedURIComponent(rawHeaderDirectory, headerEncoding) : null;
|
||||
const queryDirectory = Array.isArray(req.query?.directory)
|
||||
? req.query.directory[0]
|
||||
: req.query?.directory;
|
||||
@@ -103,7 +113,9 @@ export const createProjectDirectoryRuntime = (dependencies) => {
|
||||
};
|
||||
|
||||
const resolveOptionalProjectDirectory = async (req) => {
|
||||
const headerDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const rawHeaderDirectory = typeof req.get === 'function' ? req.get('x-opencode-directory') : null;
|
||||
const headerEncoding = typeof req.get === 'function' ? req.get('x-opencode-directory-encoding') : null;
|
||||
const headerDirectory = rawHeaderDirectory ? safeDecodeMarkedURIComponent(rawHeaderDirectory, headerEncoding) : null;
|
||||
const queryDirectory = Array.isArray(req.query?.directory)
|
||||
? req.query.directory[0]
|
||||
: req.query?.directory;
|
||||
|
||||
@@ -128,6 +128,58 @@ describe('project directory runtime', () => {
|
||||
expect(result).toEqual({ directory: '/real/workspace/project', error: null });
|
||||
});
|
||||
|
||||
it('decodes marked x-opencode-directory header values', async () => {
|
||||
const pathWithUnicode = '/home/user/测试项目';
|
||||
let validatedPath = null;
|
||||
const runtime = createTestRuntime({
|
||||
fsPromises: {
|
||||
stat: async (p) => {
|
||||
validatedPath = p;
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
realpath: async (p) => p,
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
get: (header) => {
|
||||
if (header === 'x-opencode-directory') return encodeURIComponent(pathWithUnicode);
|
||||
if (header === 'x-opencode-directory-encoding') return 'uri';
|
||||
return null;
|
||||
},
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await runtime.resolveProjectDirectory(req);
|
||||
|
||||
expect(validatedPath).toBe(pathWithUnicode);
|
||||
expect(result).toEqual({ directory: pathWithUnicode, error: null });
|
||||
});
|
||||
|
||||
it('preserves raw percent sequences without directory encoding marker', async () => {
|
||||
const rawPath = '/home/user/foo%20bar';
|
||||
let validatedPath = null;
|
||||
const runtime = createTestRuntime({
|
||||
fsPromises: {
|
||||
stat: async (p) => {
|
||||
validatedPath = p;
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
realpath: async (p) => p,
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
get: (header) => header === 'x-opencode-directory' ? rawPath : null,
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await runtime.resolveProjectDirectory(req);
|
||||
|
||||
expect(validatedPath).toBe(rawPath);
|
||||
expect(result).toEqual({ directory: rawPath, error: null });
|
||||
});
|
||||
|
||||
it('resolves symlinks in query directory parameter', async () => {
|
||||
const runtime = createTestRuntime({
|
||||
fsPromises: {
|
||||
@@ -222,5 +274,29 @@ describe('project directory runtime', () => {
|
||||
|
||||
expect(result).toEqual({ directory: '/real/workspace/project', error: null });
|
||||
});
|
||||
|
||||
it('preserves raw percent sequences without directory encoding marker', async () => {
|
||||
const rawPath = '/optional/foo%25bar';
|
||||
let validatedPath = null;
|
||||
const runtime = createTestRuntime({
|
||||
fsPromises: {
|
||||
stat: async (p) => {
|
||||
validatedPath = p;
|
||||
return { isDirectory: () => true };
|
||||
},
|
||||
realpath: async (p) => p,
|
||||
},
|
||||
});
|
||||
|
||||
const req = {
|
||||
get: (header) => header === 'x-opencode-directory' ? rawPath : null,
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await runtime.resolveOptionalProjectDirectory(req);
|
||||
|
||||
expect(validatedPath).toBe(rawPath);
|
||||
expect(result).toEqual({ directory: rawPath, error: null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user