Skip to content

Commit 2297288

Browse files
committed
improvements
1 parent e19a431 commit 2297288

4 files changed

Lines changed: 95 additions & 21 deletions

File tree

apps/cli/src/next/commands/functions/download/download.handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ export const functionsDownload = Effect.fnUntraced(function* (flags: FunctionsDo
1919
projectRoot: projectHome.projectRoot,
2020
resolveProjectRef,
2121
proxyDownload: (proxyFlags, projectRef) =>
22-
proxy.exec(makeGoProxyDownloadArgs(proxyFlags, projectRef)),
22+
proxy.exec(makeGoProxyDownloadArgs(proxyFlags, projectRef), { cwd: projectHome.projectRoot }),
2323
});
2424
});

apps/cli/src/shared/functions/download.ts

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,31 @@ function parseMultipartHeaders(rawHeaders: string): Readonly<Record<string, stri
228228
return headers;
229229
}
230230

231+
function findNextMultipartBoundary(
232+
payload: Uint8Array,
233+
boundaryPrefix: Uint8Array,
234+
from = 0,
235+
): number {
236+
let offset = from;
237+
while (offset < payload.length) {
238+
const index = findBytes(payload, boundaryPrefix, offset);
239+
if (index < 0) {
240+
return -1;
241+
}
242+
243+
const suffixIndex = index + boundaryPrefix.length;
244+
const isClosingBoundary = payload[suffixIndex] === 45 && payload[suffixIndex + 1] === 45;
245+
const isPartBoundary = payload[suffixIndex] === 13 && payload[suffixIndex + 1] === 10;
246+
if (isClosingBoundary || isPartBoundary) {
247+
return index;
248+
}
249+
250+
offset = index + 1;
251+
}
252+
253+
return -1;
254+
}
255+
231256
function decodeMultipartParts(
232257
payload: Uint8Array,
233258
boundary: string,
@@ -259,7 +284,7 @@ function decodeMultipartParts(
259284
throw new Error("multipart part is missing its header separator");
260285
}
261286
const bodyStart = separatorIndex + headerSeparator.length;
262-
const nextPartIndex = findBytes(payload, nextPartPrefix, bodyStart);
287+
const nextPartIndex = findNextMultipartBoundary(payload, nextPartPrefix, bodyStart);
263288
if (nextPartIndex < 0) {
264289
throw new Error("multipart response is missing its closing boundary");
265290
}
@@ -282,16 +307,19 @@ function decodeMultipartParts(
282307

283308
function readContentDispositionParam(
284309
contentDisposition: string,
285-
param: "name" | "filename",
310+
param: "name" | "filename" | "filename*",
286311
): Effect.Effect<string | undefined, InvalidFunctionDownloadResponseError> {
312+
const paramPattern = param.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
287313
const quotedMatch = contentDisposition.match(
288-
new RegExp(`(?:^|;)\\s*${param}="((?:[^"\\\\]|\\\\.)*)"`, "i"),
314+
new RegExp(`(?:^|;)\\s*${paramPattern}="((?:[^"\\\\]|\\\\.)*)"`, "i"),
289315
);
290316
if (quotedMatch !== null) {
291317
return Effect.succeed(quotedMatch[1]?.replaceAll('\\"', '"'));
292318
}
293319

294-
const assignmentMatch = contentDisposition.match(new RegExp(`(?:^|;)\\s*${param}=([^;]*)`, "i"));
320+
const assignmentMatch = contentDisposition.match(
321+
new RegExp(`(?:^|;)\\s*${paramPattern}=([^;]*)`, "i"),
322+
);
295323
if (assignmentMatch === null) {
296324
return Effect.succeed(undefined);
297325
}
@@ -307,6 +335,37 @@ function readContentDispositionParam(
307335
);
308336
}
309337

338+
function decodeRfc5987Param(
339+
value: string,
340+
): Effect.Effect<string, InvalidFunctionDownloadResponseError> {
341+
const firstQuote = value.indexOf("'");
342+
const secondQuote = firstQuote < 0 ? -1 : value.indexOf("'", firstQuote + 1);
343+
if (firstQuote < 0 || secondQuote < 0) {
344+
return Effect.fail(
345+
new InvalidFunctionDownloadResponseError({
346+
message: "failed to parse content disposition: malformed filename*",
347+
}),
348+
);
349+
}
350+
351+
const charset = value.slice(0, firstQuote).toLowerCase();
352+
if (charset !== "utf-8" && charset !== "us-ascii") {
353+
return Effect.fail(
354+
new InvalidFunctionDownloadResponseError({
355+
message: `failed to parse content disposition: unsupported filename* charset ${charset}`,
356+
}),
357+
);
358+
}
359+
360+
return Effect.try({
361+
try: () => decodeURIComponent(value.slice(secondQuote + 1)),
362+
catch: (cause) =>
363+
new InvalidFunctionDownloadResponseError({
364+
message: `failed to parse content disposition: malformed filename*: ${cause instanceof Error ? cause.message : String(cause)}`,
365+
}),
366+
});
367+
}
368+
310369
function readFormFieldName(
311370
headers: Readonly<Record<string, string>>,
312371
): Effect.Effect<string | undefined, InvalidFunctionDownloadResponseError> {
@@ -317,6 +376,19 @@ function readFormFieldName(
317376
return readContentDispositionParam(contentDisposition, "name");
318377
}
319378

379+
function readContentDispositionFilename(
380+
contentDisposition: string,
381+
): Effect.Effect<string | undefined, InvalidFunctionDownloadResponseError> {
382+
return Effect.gen(function* () {
383+
const encodedFilename = yield* readContentDispositionParam(contentDisposition, "filename*");
384+
if (encodedFilename !== undefined) {
385+
return yield* decodeRfc5987Param(encodedFilename);
386+
}
387+
388+
return yield* readContentDispositionParam(contentDisposition, "filename");
389+
});
390+
}
391+
320392
function getPartPath(
321393
headers: Readonly<Record<string, string>>,
322394
): Effect.Effect<string, InvalidFunctionDownloadResponseError> {
@@ -330,7 +402,7 @@ function getPartPath(
330402
return Effect.succeed("");
331403
}
332404

333-
return readContentDispositionParam(contentDisposition, "filename").pipe(
405+
return readContentDispositionFilename(contentDisposition).pipe(
334406
Effect.map((filename) => filename ?? ""),
335407
);
336408
}
@@ -357,6 +429,12 @@ function decodeMultipartForm(
357429
const files: DownloadFilePart[] = [];
358430

359431
for (const part of parts) {
432+
const filePath = yield* getPartPath(part.headers);
433+
if (filePath.length > 0) {
434+
files.push({ path: filePath, body: part.body });
435+
continue;
436+
}
437+
360438
const fieldName = yield* readFormFieldName(part.headers);
361439
if (fieldName === "metadata") {
362440
const rawMetadata = new TextDecoder().decode(part.body);
@@ -367,12 +445,6 @@ function decodeMultipartForm(
367445
message: `failed to unmarshal metadata: ${cause instanceof Error ? cause.message : String(cause)}`,
368446
}),
369447
});
370-
continue;
371-
}
372-
373-
const filePath = yield* getPartPath(part.headers);
374-
if (filePath.length > 0) {
375-
files.push({ path: filePath, body: part.body });
376448
}
377449
}
378450

@@ -459,7 +531,7 @@ const listRemoteFunctionSlugs = Effect.fnUntraced(function* (api: ApiClient, pro
459531
try: () => {
460532
const parsed = JSON.parse(body);
461533
if (!Array.isArray(parsed)) {
462-
return [];
534+
throw new Error("expected functions list response to be an array");
463535
}
464536
return parsed.flatMap((value) => {
465537
const slug = getObjectProperty(value, "slug");
@@ -468,7 +540,7 @@ const listRemoteFunctionSlugs = Effect.fnUntraced(function* (api: ApiClient, pro
468540
},
469541
catch: (cause) =>
470542
new InvalidFunctionDownloadResponseError({
471-
message: `failed to read form: ${cause instanceof Error ? cause.message : String(cause)}`,
543+
message: `failed to read functions list: ${cause instanceof Error ? cause.message : String(cause)}`,
472544
}),
473545
});
474546
});
@@ -554,10 +626,9 @@ const downloadSingle = Effect.fnUntraced(function* (
554626

555627
const response = yield* downloadBody(dependencies.api, projectRef, slug);
556628
const { metadata, files } = yield* decodeMultipartForm(response);
557-
const remoteFunction =
558-
metadata?.entrypoint_path !== undefined
559-
? undefined
560-
: yield* getRemoteFunction(dependencies.api, projectRef, slug);
629+
const remoteFunction = hasEntrypointPath(metadata)
630+
? undefined
631+
: yield* getRemoteFunction(dependencies.api, projectRef, slug);
561632
const entrypointPath = resolveEntrypointPath(metadata, remoteFunction);
562633
const projectRoot = dependencies.projectRoot;
563634
const functionsRoot = join(projectRoot, "supabase", "functions");

apps/cli/src/shared/legacy/go-proxy.layer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export function makeGoProxyLayer(opts?: {
159159
const globalArgs = opts?.globalArgs ?? [];
160160

161161
return LegacyGoProxy.of({
162-
exec: (args) =>
162+
exec: (args, execOpts) =>
163163
Effect.scoped(
164164
Effect.gen(function* () {
165165
if (!("found" in resolved)) {
@@ -198,7 +198,7 @@ export function makeGoProxyLayer(opts?: {
198198
// normal completion, failure, or fiber interruption.
199199
yield* processControl.holdSignals(["SIGINT", "SIGTERM", "SIGHUP"]);
200200
const command = ChildProcess.make(binary, [...globalArgs, ...args], {
201-
cwd: opts?.cwd,
201+
cwd: execOpts?.cwd ?? opts?.cwd,
202202
env: opts?.env,
203203
extendEnv: true,
204204
stdin: "inherit",

apps/cli/src/shared/legacy/go-proxy.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ interface LegacyGoProxyShape {
77
* and propagating the exit code. On a non-zero exit the process exits with
88
* the same code — callers do not need to handle the failure case.
99
*/
10-
readonly exec: (args: ReadonlyArray<string>) => Effect.Effect<void>;
10+
readonly exec: (
11+
args: ReadonlyArray<string>,
12+
opts?: { readonly cwd?: string },
13+
) => Effect.Effect<void>;
1114
}
1215

1316
export class LegacyGoProxy extends Context.Service<LegacyGoProxy, LegacyGoProxyShape>()(

0 commit comments

Comments
 (0)