Skip to content

Commit 1bc197e

Browse files
committed
fix(internals): add url length validation for url generation
- Introduced a maximum URL length constant and validation in the createPlaygroundURL function to ensure URLs do not exceed 32,768 characters. - Added tests to verify that errors are thrown when the URL length limit is exceeded, ensuring robust error handling in service and utils. Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 5433ff2 commit 1bc197e

4 files changed

Lines changed: 92 additions & 10 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
uses: actions/upload-pages-artifact@v5
6868
with:
6969
path: projects/pages/dist
70+
include-hidden-files: true
7071

7172
lighthouse:
7273
runs-on: ubuntu-latest

‎projects/internals/tools/src/playground/service.test.ts‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { tmpdir } from 'node:os';
77
import{afterEach,beforeEach,describe,expect,it}from'vitest';
88
import{loadTools,typeToolMethod,typeToolOutput}from'../internal/tools.js';
99
import{PlaygroundService}from'./service.js';
10-
import{createPlaygroundURL}from'./utils.js';
10+
import{createPlaygroundURL,MAX_PLAYGROUND_URL_LENGTH}from'./utils.js';
1111

1212
// when ELEMENTS_PLAYGROUND_BASE_URL is not configured, createPlaygroundURL returns ''
1313
consthasPlaygroundBaseURL=createPlaygroundURL('test',[]).length>0;
@@ -148,6 +148,30 @@ describe('PlaygroundService', () => {
148148
);
149149
});
150150

151+
it('should handle content that would exceed the supported playground URL length',async()=>{
152+
process.env.ELEMENTS_ENV='browser';
153+
consttools=loadTools(PlaygroundService);
154+
constcreateTool=tools.find(tool=>tool.metadata.name==='create');
155+
156+
constresult=(awaitcreateTool?.({
157+
template: '<nve-button>valid</nve-button>',
158+
name: 'x'.repeat(MAX_PLAYGROUND_URL_LENGTH),
159+
start: false
160+
}))asToolOutput<string>;
161+
162+
if(!hasPlaygroundBaseURL){
163+
expect(result.status).toBe('complete');
164+
expect(result.result).toBe('');
165+
return;
166+
}
167+
168+
expect(result.status).toBe('error');
169+
expect(result.message).toBe(
170+
`Playground content produces a URL that exceeds the ${MAX_PLAYGROUND_URL_LENGTH}-character limit.`
171+
);
172+
expect(result.result).toBeUndefined();
173+
});
174+
151175
it('should skip validation and return URL when not in mcp or cli environment',async()=>{
152176
process.env.ELEMENTS_ENV='browser';
153177
constresult=awaitPlaygroundService.create({

‎projects/internals/tools/src/playground/utils.test.ts‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import{describe,it,expect}from'vitest';
55
importtype{ProjectElement}from'@internals/metadata';
6+
import{ToolError}from'../internal/tools.js';
67
import{
78
createPlaygroundURL,
89
createAngularFiles,
@@ -12,6 +13,7 @@ import {
1213
createVueFiles,
1314
createDefaultFiles,
1415
formatTemplate,
16+
MAX_PLAYGROUND_URL_LENGTH,
1517
playgroundTypes
1618
}from'./utils.js';
1719

@@ -26,6 +28,20 @@ function expectURL(result: string, expected: string) {
2628
}
2729
}
2830

31+
functioncreateDeterministicPseudorandomBytes(length: number){
32+
constbytes=newUint8Array(length);
33+
letstate=0x12345678;
34+
35+
for(letindex=0;index<bytes.length;index+=1){
36+
state^=state<<13;
37+
state^=state>>>17;
38+
state^=state<<5;
39+
bytes[index]=state;
40+
}
41+
42+
returnbytes;
43+
}
44+
2945
describe('createPlaygroundURL',()=>{
3046
constelements: ProjectElement[]=[
3147
{
@@ -558,17 +574,39 @@ describe('createImportMap with different frameworks', () => {
558574

559575
describe('serialize function behavior',()=>{
560576
it('should compress and encode data by default',()=>{
561-
// The serialize function is called internally, so we test its effect
562577
constresult=createPlaygroundURL('<nve-button></nve-button>',[],{});
563578

564579
if(hasPlaygroundBaseURL){
565-
// Should contain encoded files data
566580
expect(result).toContain('&files=');
567581
expect(result.length).toBeGreaterThan(100);// Should be reasonably long due to compression
568582
}else{
569583
expect(result).toBe('');
570584
}
571585
});
586+
587+
it('should reject playground URLs that exceed the supported V8 argument-count limit',()=>{
588+
constbytes=createDeterministicPseudorandomBytes(200_000);
589+
constattributeValue=Buffer.from(bytes).toString('base64').replace(/[+/=]/g,'A');
590+
consttemplate=`<div data-value="${attributeValue}">content</div>`;
591+
592+
if(!hasPlaygroundBaseURL){
593+
expect(createPlaygroundURL(template,[])).toBe('');
594+
return;
595+
}
596+
597+
letthrown: unknown;
598+
try{
599+
createPlaygroundURL(template,[]);
600+
}catch(error){
601+
thrown=error;
602+
}
603+
604+
expect(thrown).toBeInstanceOf(ToolError);
605+
expect(thrown).toHaveProperty(
606+
'message',
607+
`Playground content produces a URL that exceeds the ${MAX_PLAYGROUND_URL_LENGTH}-character limit.`
608+
);
609+
});
572610
});
573611

574612
describe('Edge cases and error handling',()=>{

‎projects/internals/tools/src/playground/utils.ts‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import{gzipSync}from'fflate';
55
importformatfrom'html-format';
66
importtype{Element}from'@internals/metadata';
7+
import{ToolError}from'../internal/tools.js';
78
import{getElementImports}from'../internal/utils.js';
89
import{validateTemplate}from'../internal/validate.js';
910

1011
declareconst__ELEMENTS_ESM_CDN_BASE_URL__: string;
1112

1213
constELEMENTS_PLAYGROUND_BASE_URL=process.env.ELEMENTS_PLAYGROUND_BASE_URL??'';
1314
constELEMENTS_ESM_CDN_BASE_URL=__ELEMENTS_ESM_CDN_BASE_URL__;
15+
exportconstMAX_PLAYGROUND_URL_LENGTH=32_768;
1416

1517
interfacePlaygroundOptions{
1618
type?: PlaygroundType;
@@ -147,12 +149,22 @@ export function createVueFiles(content: string, elements: Element[], options: Pl
147149
}
148150

149151
functioncreateURL(files: string,options: PlaygroundOptions){
152+
if(ELEMENTS_PLAYGROUND_BASE_URL.length===0){
153+
return'';
154+
}
155+
150156
constdefaultOptions={openFile: 'index.html', ...options};
151-
returnELEMENTS_PLAYGROUND_BASE_URL.length>0
152-
? encodeURI(
153-
`${ELEMENTS_PLAYGROUND_BASE_URL}/?version=1&layout=vertical-split${defaultOptions.name ? `&name=${defaultOptions.name.trim()}` : ''}${defaultOptions.theme ? `&theme=${defaultOptions.theme}` : ''}&file=${defaultOptions.openFile}${defaultOptions.referer ? `&ref=${defaultOptions.referer}` : ''}&files=${files}`
154-
)
155-
: '';
157+
consturl=encodeURI(
158+
`${ELEMENTS_PLAYGROUND_BASE_URL}/?version=1&layout=vertical-split${defaultOptions.name ? `&name=${defaultOptions.name.trim()}` : ''}${defaultOptions.theme ? `&theme=${defaultOptions.theme}` : ''}&file=${defaultOptions.openFile}${defaultOptions.referer ? `&ref=${defaultOptions.referer}` : ''}&files=${files}`
159+
);
160+
161+
if(url.length>MAX_PLAYGROUND_URL_LENGTH){
162+
thrownewToolError(
163+
`Playground content produces a URL that exceeds the ${MAX_PLAYGROUND_URL_LENGTH}-character limit.`
164+
);
165+
}
166+
167+
returnurl;
156168
}
157169

158170
functioncreateLayoutStyles(){
@@ -195,8 +207,15 @@ nve-logo.large {
195207
functionserialize(data: Record<string,{content: string}>,compress=true){
196208
constencoded=newTextEncoder().encode(JSON.stringify(data));
197209
constarray=compress ? gzipSync(encoded) : encoded;
198-
constbase64=globalThis.btoa(String.fromCharCode(...array));
199-
returnencodeURIComponent(base64);
210+
// Limit each function call to 32,768 arguments, safely below engine limits.
211+
constchunkSize=0x8000;
212+
letbinary='';
213+
214+
for(letoffset=0;offset<array.length;offset+=chunkSize){
215+
binary+=String.fromCharCode(...array.subarray(offset,offset+chunkSize));
216+
}
217+
218+
returnencodeURIComponent(globalThis.btoa(binary));
200219
}
201220

202221
functioncreateIndexHTML(content: string,options: PlaygroundOptions){

0 commit comments

Comments
 (0)