@@ -187,36 +187,75 @@ const isChildPath = (parent, child) => {
187
187
return ! ! relative && ! relative . startsWith ( '..' ) && ! path . isAbsolute ( relative ) ;
188
188
} ;
189
189
190
+ /** @type {Set<string> } */
191
+ const allFileIDs = new Set ( ) ;
192
+
193
+ /**
194
+ * @returns {string } A unique string.
195
+ */
196
+ const generateFileId = ( ) => {
197
+ let result ;
198
+ let tries = 0 ;
199
+
200
+ do {
201
+ tries ++ ;
202
+ if ( tries > 50 ) {
203
+ // Should never happen...
204
+ throw new Error ( 'Failed to generate file ID' ) ;
205
+ }
206
+
207
+ result = 'desktop_file_id{' ;
208
+
209
+ // >200 bits of randomness; impractical to brute force.
210
+ // Math.random() is not cryptographically secure, but even if someone can reverse it, they would
211
+ // still only be able to access files that were already opened, so impact is not that big.
212
+ const soup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' ;
213
+ for ( let i = 0 ; i < 40 ; i ++ ) {
214
+ result += soup [ Math . floor ( Math . random ( ) * soup . length ) ] ;
215
+ }
216
+
217
+ result += '}' ;
218
+ } while ( allFileIDs . has ( result ) ) ;
219
+
220
+ allFileIDs . add ( result ) ;
221
+ return result ;
222
+ } ;
223
+
190
224
class EditorWindow extends ProjectRunningWindow {
191
225
/**
192
- * @param {OpenedFile|null } file
226
+ * @param {OpenedFile|null } initialFile
193
227
* @param {boolean } isInitiallyFullscreen
194
228
*/
195
- constructor ( file , isInitiallyFullscreen ) {
229
+ constructor ( initialFile , isInitiallyFullscreen ) {
196
230
super ( ) ;
197
231
198
- // This file ID system is not quite perfect. Ideally we would completely revoke permission to access
199
- // old projects after you load the next one, but our handling of file handles in scratch-gui is
200
- // pretty bad right now, so this is the best compromise.
201
- this . openedFiles = [ ] ;
202
- this . activeFileIndex = - 1 ;
232
+ /**
233
+ * Ideally we would revoke access after loading a new project, but our file handle handling in
234
+ * the GUI isn't robust enough for that yet. We do at least use random file handle IDs which
235
+ * makes it much harder for malicious code in the renderer process to enumerate all previously
236
+ * opened IDs and overwrite them.
237
+ * @type {Map<string, OpenedFile> }
238
+ */
239
+ this . openedFiles = new Map ( ) ;
240
+ this . activeFileId = null ;
203
241
204
- if ( file !== null ) {
205
- this . openedFiles . push ( file ) ;
206
- this . activeFileIndex = 0 ;
242
+ if ( initialFile !== null ) {
243
+ this . activeFileId = generateFileId ( ) ;
244
+ this . openedFiles . set ( this . activeFileId , initialFile ) ;
207
245
}
208
246
209
247
this . openedProjectAt = Date . now ( ) ;
210
248
211
- const getFileByIndex = ( index ) => {
212
- if ( typeof index !== 'number' ) {
213
- throw new Error ( 'File ID not number' ) ;
214
- }
215
- const value = this . openedFiles [ index ] ;
216
- if ( ! ( value instanceof OpenedFile ) ) {
249
+ /**
250
+ * @param {string } id
251
+ * @returns {OpenedFile }
252
+ * @throws if invalid ID
253
+ */
254
+ const getFileById = ( id ) => {
255
+ if ( ! this . openedFiles . has ( id ) ) {
217
256
throw new Error ( 'Invalid file ID' ) ;
218
257
}
219
- return this . openedFiles [ index ] ;
258
+ return this . openedFiles . get ( id ) ;
220
259
} ;
221
260
222
261
this . window . webContents . on ( 'will-prevent-unload' , ( event ) => {
@@ -263,14 +302,11 @@ class EditorWindow extends ProjectRunningWindow {
263
302
} ) ;
264
303
265
304
ipc . handle ( 'get-initial-file' , ( ) => {
266
- if ( this . activeFileIndex === - 1 ) {
267
- return null ;
268
- }
269
- return this . activeFileIndex ;
305
+ return this . activeFileId ;
270
306
} ) ;
271
307
272
- ipc . handle ( 'get-file' , async ( event , index ) => {
273
- const file = getFileByIndex ( index ) ;
308
+ ipc . handle ( 'get-file' , async ( event , id ) => {
309
+ const file = getFileById ( id ) ;
274
310
const { name, data} = await file . read ( ) ;
275
311
return {
276
312
name,
@@ -301,18 +337,18 @@ class EditorWindow extends ProjectRunningWindow {
301
337
this . window . setDocumentEdited ( changed ) ;
302
338
} ) ;
303
339
304
- ipc . handle ( 'opened-file' , ( event , index ) => {
305
- const file = getFileByIndex ( index ) ;
340
+ ipc . handle ( 'opened-file' , ( event , id ) => {
341
+ const file = getFileById ( id ) ;
306
342
if ( file . type !== TYPE_FILE ) {
307
343
throw new Error ( 'Not a file' ) ;
308
344
}
309
- this . activeFileIndex = index ;
345
+ this . activeFileId = id ;
310
346
this . openedProjectAt = Date . now ( ) ;
311
347
this . window . setRepresentedFilename ( file . path ) ;
312
348
} ) ;
313
349
314
350
ipc . handle ( 'closed-file' , ( ) => {
315
- this . activeFileIndex = - 1 ;
351
+ this . activeFileId = null ;
316
352
this . window . setRepresentedFilename ( '' ) ;
317
353
} ) ;
318
354
@@ -331,14 +367,16 @@ class EditorWindow extends ProjectRunningWindow {
331
367
return null ;
332
368
}
333
369
334
- const file = result . filePaths [ 0 ] ;
335
- settings . lastDirectory = path . dirname ( file ) ;
370
+ const filePath = result . filePaths [ 0 ] ;
371
+ settings . lastDirectory = path . dirname ( filePath ) ;
336
372
await settings . save ( ) ;
337
373
338
- this . openedFiles . push ( new OpenedFile ( TYPE_FILE , file ) ) ;
374
+ const id = generateFileId ( ) ;
375
+ this . openedFiles . set ( id , new OpenedFile ( TYPE_FILE , filePath ) ) ;
376
+
339
377
return {
340
- id : this . openedFiles . length - 1 ,
341
- name : path . basename ( file )
378
+ id,
379
+ name : path . basename ( filePath )
342
380
} ;
343
381
} ) ;
344
382
@@ -356,30 +394,32 @@ class EditorWindow extends ProjectRunningWindow {
356
394
return null ;
357
395
}
358
396
359
- const file = result . filePath ;
397
+ const filePath = result . filePath ;
360
398
361
- const unsafePath = getUnsafePaths ( ) . find ( i => isChildPath ( i . path , file ) ) ;
399
+ const unsafePath = getUnsafePaths ( ) . find ( i => isChildPath ( i . path , filePath ) ) ;
362
400
if ( unsafePath ) {
363
- // No need to wait for the message box to close
401
+ // No need to block until the message box is closed
364
402
dialog . showMessageBox ( this . window , {
365
403
type : 'error' ,
366
404
title : APP_NAME ,
367
405
message : translate ( 'unsafe-path.title' ) ,
368
406
detail : translate ( `unsafe-path.details` )
369
407
. replace ( '{APP_NAME}' , unsafePath . app )
370
- . replace ( '{file}' , file ) ,
408
+ . replace ( '{file}' , filePath ) ,
371
409
noLink : true
372
410
} ) ;
373
411
return null ;
374
412
}
375
413
376
- settings . lastDirectory = path . dirname ( file ) ;
414
+ settings . lastDirectory = path . dirname ( filePath ) ;
377
415
await settings . save ( ) ;
378
416
379
- this . openedFiles . push ( new OpenedFile ( TYPE_FILE , file ) ) ;
417
+ const id = generateFileId ( ) ;
418
+ this . openedFiles . set ( id , new OpenedFile ( TYPE_FILE , filePath ) ) ;
419
+
380
420
return {
381
- id : this . openedFiles . length - 1 ,
382
- name : path . basename ( file )
421
+ id,
422
+ name : path . basename ( filePath )
383
423
} ;
384
424
} ) ;
385
425
@@ -390,8 +430,8 @@ class EditorWindow extends ProjectRunningWindow {
390
430
} ;
391
431
} ) ;
392
432
393
- ipc . on ( 'start-write-stream' , async ( startEvent , index ) => {
394
- const file = getFileByIndex ( index ) ;
433
+ ipc . on ( 'start-write-stream' , async ( startEvent , id ) => {
434
+ const file = getFileById ( id ) ;
395
435
if ( file . type !== TYPE_FILE ) {
396
436
throw new Error ( 'Not a file' ) ;
397
437
}
0 commit comments