summaryrefslogtreecommitdiffhomepage
path: root/packages/sdk/src/internal/utils/path.ts
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-07-22 11:50:51 -0500
committeradamdotdevin <[email protected]>2025-07-22 11:50:51 -0500
commit10c8b495907069461f5dc464f6285321290c8b14 (patch)
tree9fed07910dd7d99688c0261a364d810a500e21d3 /packages/sdk/src/internal/utils/path.ts
parent500cea5ce7fa635a924cd9abea63aaf672f7645d (diff)
downloadopencode-10c8b495907069461f5dc464f6285321290c8b14.tar.gz
opencode-10c8b495907069461f5dc464f6285321290c8b14.zip
chore: generate sdk into packages/sdk
Diffstat (limited to 'packages/sdk/src/internal/utils/path.ts')
-rw-r--r--packages/sdk/src/internal/utils/path.ts88
1 files changed, 88 insertions, 0 deletions
diff --git a/packages/sdk/src/internal/utils/path.ts b/packages/sdk/src/internal/utils/path.ts
new file mode 100644
index 000000000..6ca96e0dd
--- /dev/null
+++ b/packages/sdk/src/internal/utils/path.ts
@@ -0,0 +1,88 @@
+import { OpencodeError } from '../../core/error';
+
+/**
+ * Percent-encode everything that isn't safe to have in a path without encoding safe chars.
+ *
+ * Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3:
+ * > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ * > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+ * > pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ */
+export function encodeURIPath(str: string) {
+ return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
+}
+
+const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
+
+export const createPathTagFunction = (pathEncoder = encodeURIPath) =>
+ function path(statics: readonly string[], ...params: readonly unknown[]): string {
+ // If there are no params, no processing is needed.
+ if (statics.length === 1) return statics[0]!;
+
+ let postPath = false;
+ const invalidSegments = [];
+ const path = statics.reduce((previousValue, currentValue, index) => {
+ if (/[?#]/.test(currentValue)) {
+ postPath = true;
+ }
+ const value = params[index];
+ let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
+ if (
+ index !== params.length &&
+ (value == null ||
+ (typeof value === 'object' &&
+ // handle values from other realms
+ value.toString ===
+ Object.getPrototypeOf(Object.getPrototypeOf((value as any).hasOwnProperty ?? EMPTY) ?? EMPTY)
+ ?.toString))
+ ) {
+ encoded = value + '';
+ invalidSegments.push({
+ start: previousValue.length + currentValue.length,
+ length: encoded.length,
+ error: `Value of type ${Object.prototype.toString
+ .call(value)
+ .slice(8, -1)} is not a valid path parameter`,
+ });
+ }
+ return previousValue + currentValue + (index === params.length ? '' : encoded);
+ }, '');
+
+ const pathOnly = path.split(/[?#]/, 1)[0]!;
+ const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
+ let match;
+
+ // Find all invalid segments
+ while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
+ invalidSegments.push({
+ start: match.index,
+ length: match[0].length,
+ error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
+ });
+ }
+
+ invalidSegments.sort((a, b) => a.start - b.start);
+
+ if (invalidSegments.length > 0) {
+ let lastEnd = 0;
+ const underline = invalidSegments.reduce((acc, segment) => {
+ const spaces = ' '.repeat(segment.start - lastEnd);
+ const arrows = '^'.repeat(segment.length);
+ lastEnd = segment.start + segment.length;
+ return acc + spaces + arrows;
+ }, '');
+
+ throw new OpencodeError(
+ `Path parameters result in path with invalid segments:\n${invalidSegments
+ .map((e) => e.error)
+ .join('\n')}\n${path}\n${underline}`,
+ );
+ }
+
+ return path;
+ };
+
+/**
+ * URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
+ */
+export const path = /* @__PURE__ */ createPathTagFunction(encodeURIPath);