Skip to content

Commit b4835cf

Browse files
refactor: address comments.
1 parent e0d8b67 commit b4835cf

4 files changed

Lines changed: 63 additions & 8 deletions

File tree

playwright/app.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type PullRequestCreateBody = {
3030
const waitForAppReady = async (page: Page, path = appEntryPath) => {
3131
await page.goto(path)
3232
await expect(page.getByRole('heading', { name: '@knighted/develop' })).toBeVisible()
33+
await expect(page.locator('#cdn-loading')).toHaveAttribute('hidden', '')
34+
await expect.poll(() => page.locator('#status').textContent()).not.toBe('Idle')
3335
}
3436

3537
const waitForInitialRender = async (page: Page) => {
@@ -702,6 +704,37 @@ test('Open PR drawer validates unsafe filepaths', async ({ page }) => {
702704
await expect(page.locator('#clear-confirm-dialog')).not.toHaveAttribute('open', '')
703705
})
704706

707+
test('Open PR drawer allows dotted file segments that are not traversal', async ({
708+
page,
709+
}) => {
710+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
711+
await connectByotWithSingleRepo(page)
712+
await ensureOpenPrDrawerOpen(page)
713+
714+
await page.locator('#github-pr-component-path').fill('docs/v1.0..v1.1/App.tsx')
715+
await page.locator('#github-pr-styles-path').fill('styles/foo..bar.css')
716+
await page.locator('#github-pr-submit').click()
717+
718+
await expect(page.locator('#clear-confirm-dialog')).toHaveAttribute('open', '')
719+
await expect(page.locator('#github-pr-status')).not.toContainText(
720+
'File path cannot include parent directory traversal.',
721+
)
722+
})
723+
724+
test('Open PR drawer rejects trailing slash file paths', async ({ page }) => {
725+
await waitForAppReady(page, `${appEntryPath}?feature-ai=true`)
726+
await connectByotWithSingleRepo(page)
727+
await ensureOpenPrDrawerOpen(page)
728+
729+
await page.locator('#github-pr-component-path').fill('src/components/')
730+
await page.locator('#github-pr-submit').click()
731+
732+
await expect(page.locator('#github-pr-status')).toContainText(
733+
'Component path: File path must include a filename (no trailing slash).',
734+
)
735+
await expect(page.locator('#clear-confirm-dialog')).not.toHaveAttribute('open', '')
736+
})
737+
705738
test('renders default playground preview', async ({ page }) => {
706739
await waitForInitialRender(page)
707740

src/modules/github-api.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -545,9 +545,14 @@ const requestGitHubJson = async ({
545545
signal,
546546
allowNotFound = false,
547547
}) => {
548+
const headers = {
549+
...buildRequestHeaders(token),
550+
...(body ? { 'Content-Type': 'application/json' } : {}),
551+
}
552+
548553
const response = await fetch(url, {
549554
method,
550-
headers: buildRequestHeaders(token),
555+
headers,
551556
body: body ? JSON.stringify(body) : undefined,
552557
signal,
553558
})
@@ -636,13 +641,15 @@ export const getRepositoryFileMetadata = async ({
636641
const toUtf8Base64 = value => {
637642
const encoder = new TextEncoder()
638643
const bytes = encoder.encode(value)
639-
let binary = ''
644+
const chunkSize = 0x8000
645+
const chunks = []
640646

641-
for (const byte of bytes) {
642-
binary += String.fromCharCode(byte)
647+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
648+
const chunk = bytes.subarray(offset, offset + chunkSize)
649+
chunks.push(String.fromCharCode(...chunk))
643650
}
644651

645-
return btoa(binary)
652+
return btoa(chunks.join(''))
646653
}
647654

648655
const isMissingShaForExistingFileError = error => {

src/modules/github-byot-controls.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ export const createGitHubByotControls = ({
193193
}
194194

195195
const clearRepoOptions = placeholderLabel => {
196+
if (repoSelect instanceof HTMLSelectElement) {
197+
repoSelect.replaceChildren(
198+
createDefaultRepoOption({
199+
label: placeholderLabel,
200+
disabled: true,
201+
}),
202+
)
203+
repoSelect.disabled = true
204+
}
205+
196206
if (typeof onWritableRepositoriesChange === 'function') {
197207
onWritableRepositoriesChange({
198208
repositories: [],

src/modules/github-pr-drawer.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,13 @@ const validateFilePath = value => {
7171
}
7272
}
7373

74-
if (path.includes('..')) {
74+
if (path.endsWith('/')) {
75+
return { ok: false, reason: 'File path must include a filename (no trailing slash).' }
76+
}
77+
78+
const segments = path.split('/').filter(Boolean)
79+
80+
if (segments.some(segment => segment === '..')) {
7581
return { ok: false, reason: 'File path cannot include parent directory traversal.' }
7682
}
7783

@@ -83,7 +89,6 @@ const validateFilePath = value => {
8389
}
8490
}
8591

86-
const segments = path.split('/').filter(Boolean)
8792
if (segments.length === 0 || segments.some(segment => segment === '.' || !segment)) {
8893
return { ok: false, reason: 'File path is invalid.' }
8994
}
@@ -99,7 +104,7 @@ const sanitizeBranchPart = value => {
99104

100105
return trimmed
101106
.replace(/[^a-z0-9._/-]/g, '-')
102-
.replace(/\/+/, '/')
107+
.replace(/\/+/g, '/')
103108
.replace(/-{2,}/g, '-')
104109
.replace(/^[-/.]+|[-/.]+$/g, '')
105110
}

0 commit comments

Comments
 (0)