Skip to content

Commit 29bde31

Browse files
committed
debug: initial work on memory support
This implements the core memory "model" for debugging that reflects DAP, in `debugModel.ts`. It also implements a filesystem provider based on that in `debugMemory.ts`, for tentative application in the hex editor. Finally it adds context menu items for these. This works with changes in mock debug, but for some reason reopening the ".bin" file in the hex editor results in a blank editor. Still need to look at that. Ultimately though, as indicated in #126268, we'll probably want custom commands for the hex editor to call as low level read/write is not supported in the stable API. Also, the file API doesn't represent the "unreadable" ranges which DAP supports.
1 parent 30627dc commit 29bde31

File tree

18 files changed

+949
-87
lines changed

18 files changed

+949
-87
lines changed

src/vs/base/common/buffer.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,104 @@ export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReada
284284
export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream {
285285
return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks));
286286
}
287+
288+
/** Decodes base64 to a uint8 array. URL-encoded and unpadded base64 is allowed. */
289+
export function decodeBase64(encoded: string) {
290+
let building = 0;
291+
let remainder = 0;
292+
let bufi = 0;
293+
294+
// The simpler way to do this is `Uint8Array.from(atob(str), c => c.charCodeAt(0))`,
295+
// but that's about 10-20x slower than this function in current Chromium versions.
296+
297+
const buffer = new Uint8Array(Math.floor(encoded.length / 4 * 3));
298+
const append = (value: number) => {
299+
switch (remainder) {
300+
case 3:
301+
buffer[bufi++] = building | value;
302+
remainder = 0;
303+
break;
304+
case 2:
305+
buffer[bufi++] = building | (value >>> 2);
306+
building = value << 6;
307+
remainder = 3;
308+
break;
309+
case 1:
310+
buffer[bufi++] = building | (value >>> 4);
311+
building = value << 4;
312+
remainder = 2;
313+
break;
314+
default:
315+
building = value << 2;
316+
remainder = 1;
317+
}
318+
};
319+
320+
for (let i = 0; i < encoded.length; i++) {
321+
const code = encoded.charCodeAt(i);
322+
// See https://datatracker.ietf.org/doc/html/rfc4648#section-4
323+
// This branchy code is about 3x faster than an indexOf on a base64 char string.
324+
if (code >= 65 && code <= 90) {
325+
append(code - 65); // A-Z starts ranges from char code 65 to 90
326+
} else if (code >= 97 && code <= 122) {
327+
append(code - 97 + 26); // a-z starts ranges from char code 97 to 122, starting at byte 26
328+
} else if (code >= 48 && code <= 57) {
329+
append(code - 48 + 52); // 0-9 starts ranges from char code 48 to 58, starting at byte 52
330+
} else if (code === 43 || code === 45) {
331+
append(62); // "+" or "-" for URLS
332+
} else if (code === 47 || code === 95) {
333+
append(63); // "/" or "_" for URLS
334+
} else if (code === 61) {
335+
break; // "="
336+
} else {
337+
throw new SyntaxError(`Unexpected base64 character ${encoded[i]}`);
338+
}
339+
}
340+
341+
const unpadded = bufi;
342+
while (remainder > 0) {
343+
append(0);
344+
}
345+
346+
// slice is needed to account for overestimation due to padding
347+
return VSBuffer.wrap(buffer).slice(0, unpadded);
348+
}
349+
350+
const base64Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
351+
const base64UrlSafeAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
352+
353+
/** Encodes a buffer to a base64 string. */
354+
export function encodeBase64({ buffer }: VSBuffer, padded = true, urlSafe = false) {
355+
const dictionary = urlSafe ? base64UrlSafeAlphabet : base64Alphabet;
356+
let output = '';
357+
358+
const remainder = buffer.byteLength % 3;
359+
360+
let i = 0;
361+
for (; i < buffer.byteLength - remainder; i += 3) {
362+
const a = buffer[i + 0];
363+
const b = buffer[i + 1];
364+
const c = buffer[i + 2];
365+
366+
output += dictionary[a >>> 2];
367+
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
368+
output += dictionary[(b << 2 | c >>> 6) & 0b111111];
369+
output += dictionary[c & 0b111111];
370+
}
371+
372+
if (remainder === 1) {
373+
const a = buffer[i + 0];
374+
output += dictionary[a >>> 2];
375+
output += dictionary[(a << 4) & 0b111111];
376+
if (padded) { output += '=='; }
377+
} else if (remainder === 2) {
378+
const a = buffer[i + 0];
379+
const b = buffer[i + 1];
380+
output += dictionary[a >>> 2];
381+
output += dictionary[(a << 4 | b >>> 4) & 0b111111];
382+
output += dictionary[(b << 2) & 0b111111];
383+
if (padded) { output += '='; }
384+
}
385+
386+
return output;
387+
}

src/vs/base/test/common/buffer.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as assert from 'assert';
77
import { timeout } from 'vs/base/common/async';
8-
import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer';
8+
import { bufferedStreamToBuffer, bufferToReadable, bufferToStream, decodeBase64, encodeBase64, newWriteableBufferStream, readableToBuffer, streamToBuffer, VSBuffer } from 'vs/base/common/buffer';
99
import { peekStream } from 'vs/base/common/stream';
1010

1111
suite('Buffer', () => {
@@ -412,4 +412,53 @@ suite('Buffer', () => {
412412
assert.strictEqual(u2[0], 17);
413413
}
414414
});
415+
416+
suite('base64', () => {
417+
/*
418+
Generated with:
419+
420+
const crypto = require('crypto');
421+
422+
for (let i = 0; i < 16; i++) {
423+
const buf = crypto.randomBytes(i);
424+
console.log(`[new Uint8Array([${Array.from(buf).join(', ')}]), '${buf.toString('base64')}'],`)
425+
}
426+
427+
*/
428+
429+
const testCases: [Uint8Array, string][] = [
430+
[new Uint8Array([]), ''],
431+
[new Uint8Array([56]), 'OA=='],
432+
[new Uint8Array([209, 4]), '0QQ='],
433+
[new Uint8Array([19, 57, 119]), 'Ezl3'],
434+
[new Uint8Array([199, 237, 207, 112]), 'x+3PcA=='],
435+
[new Uint8Array([59, 193, 173, 26, 242]), 'O8GtGvI='],
436+
[new Uint8Array([81, 226, 95, 231, 116, 126]), 'UeJf53R+'],
437+
[new Uint8Array([11, 164, 253, 85, 8, 6, 56]), 'C6T9VQgGOA=='],
438+
[new Uint8Array([164, 16, 88, 88, 224, 173, 144, 114]), 'pBBYWOCtkHI='],
439+
[new Uint8Array([0, 196, 99, 12, 21, 229, 78, 101, 13]), 'AMRjDBXlTmUN'],
440+
[new Uint8Array([167, 114, 225, 116, 226, 83, 51, 48, 88, 114]), 'p3LhdOJTMzBYcg=='],
441+
[new Uint8Array([75, 33, 118, 10, 77, 5, 168, 194, 59, 47, 59]), 'SyF2Ck0FqMI7Lzs='],
442+
[new Uint8Array([203, 182, 165, 51, 208, 27, 123, 223, 112, 198, 127, 147]), 'y7alM9Abe99wxn+T'],
443+
[new Uint8Array([154, 93, 222, 41, 117, 234, 250, 85, 95, 144, 16, 94, 18]), 'ml3eKXXq+lVfkBBeEg=='],
444+
[new Uint8Array([246, 186, 88, 105, 192, 57, 25, 168, 183, 164, 103, 162, 243, 56]), '9rpYacA5Gai3pGei8zg='],
445+
[new Uint8Array([149, 240, 155, 96, 30, 55, 162, 172, 191, 187, 33, 124, 169, 183, 254]), 'lfCbYB43oqy/uyF8qbf+'],
446+
];
447+
448+
test('encodes', () => {
449+
for (const [bytes, expected] of testCases) {
450+
assert.strictEqual(encodeBase64(VSBuffer.wrap(bytes)), expected);
451+
}
452+
});
453+
454+
test('decodes', () => {
455+
for (const [expected, encoded] of testCases) {
456+
assert.deepStrictEqual(new Uint8Array(decodeBase64(encoded).buffer), expected);
457+
}
458+
});
459+
460+
test('throws error on invalid encoding', () => {
461+
assert.throws(() => decodeBase64('invalid!'));
462+
});
463+
});
415464
});

src/vs/base/test/common/mock.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ export function mock<T>(): Ctor<T> {
1313
return function () { } as any;
1414
}
1515

16-
export type MockObject<T, TP = {}> = { [K in keyof T]: K extends keyof TP ? TP[K] : SinonStub };
16+
export type MockObject<T, ExceptProps = never> = { [K in keyof T]: K extends ExceptProps ? T[K] : SinonStub };
1717

1818
// Creates an object object that returns sinon mocks for every property. Optionally
1919
// takes base properties.
20-
export function mockObject<T extends object, TP extends Partial<T>>(properties?: TP): MockObject<T, TP> {
20+
export const mockObject = <T extends object>() => <TP extends Partial<T> = {}>(properties?: TP): MockObject<T, keyof TP> => {
2121
return new Proxy({ ...properties } as any, {
2222
get(target, key) {
2323
if (!target.hasOwnProperty(key)) {
@@ -31,4 +31,4 @@ export function mockObject<T extends object, TP extends Partial<T>>(properties?:
3131
return true;
3232
},
3333
});
34-
}
34+
};

src/vs/workbench/contrib/debug/browser/debug.contribution.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView'
1616
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
1717
import {
1818
IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA,
19-
CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY,
19+
CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, getStateLabel, State, CONTEXT_WATCH_ITEM_TYPE, CONTEXT_STACK_FRAME_SUPPORTS_RESTART, CONTEXT_BREAK_WHEN_VALUE_IS_READ_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_IS_ACCESSED_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, DISASSEMBLY_VIEW_ID, CONTEXT_SET_EXPRESSION_SUPPORTED, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_CAN_VIEW_MEMORY,
2020
} from 'vs/workbench/contrib/debug/common/debug';
2121
import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar';
2222
import { DebugService } from 'vs/workbench/contrib/debug/browser/debugService';
@@ -32,7 +32,7 @@ import { launchSchemaId } from 'vs/workbench/services/configuration/common/confi
3232
import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView';
3333
import { RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions';
3434
import { WatchExpressionsView, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, ADD_WATCH_ID } from 'vs/workbench/contrib/debug/browser/watchExpressionsView';
35-
import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID } from 'vs/workbench/contrib/debug/browser/variablesView';
35+
import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID, BREAK_WHEN_VALUE_IS_ACCESSED_ID, BREAK_WHEN_VALUE_IS_READ_ID, VIEW_MEMORY_ID } from 'vs/workbench/contrib/debug/browser/variablesView';
3636
import { Repl } from 'vs/workbench/contrib/debug/browser/repl';
3737
import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider';
3838
import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView';
@@ -143,6 +143,7 @@ registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.lo
143143
registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), undefined, '3_modification');
144144

145145
registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, ContextKeyExpr.or(CONTEXT_SET_VARIABLE_SUPPORTED, ContextKeyExpr.and(CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, CONTEXT_SET_EXPRESSION_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification');
146+
registerDebugViewMenuItem(MenuId.DebugVariablesContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 15, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification');
146147
registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 10, undefined, undefined, '5_cutcopypaste');
147148
registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 20, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '5_cutcopypaste');
148149
registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 100, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, 'z_commands');
@@ -154,6 +155,7 @@ registerDebugViewMenuItem(MenuId.DebugWatchContext, ADD_WATCH_ID, ADD_WATCH_LABE
154155
registerDebugViewMenuItem(MenuId.DebugWatchContext, EDIT_EXPRESSION_COMMAND_ID, nls.localize('editWatchExpression', "Edit Expression"), 20, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, '3_modification');
155156
registerDebugViewMenuItem(MenuId.DebugWatchContext, SET_EXPRESSION_COMMAND_ID, nls.localize('setValue', "Set Value"), 30, ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_SET_EXPRESSION_SUPPORTED), ContextKeyExpr.and(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable'), CONTEXT_SET_VARIABLE_SUPPORTED)), CONTEXT_VARIABLE_IS_READONLY.toNegated(), '3_modification');
156157
registerDebugViewMenuItem(MenuId.DebugWatchContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 40, ContextKeyExpr.or(CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), CONTEXT_WATCH_ITEM_TYPE.isEqualTo('variable')), CONTEXT_IN_DEBUG_MODE, '3_modification');
158+
registerDebugViewMenuItem(MenuId.DebugWatchContext, VIEW_MEMORY_ID, nls.localize('viewMemory', "View Memory"), 50, CONTEXT_CAN_VIEW_MEMORY, CONTEXT_IN_DEBUG_MODE, '3_modification');
157159
registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_EXPRESSION_COMMAND_ID, nls.localize('removeWatchExpression', "Remove Expression"), 10, CONTEXT_WATCH_ITEM_TYPE.isEqualTo('expression'), undefined, 'z_commands');
158160
registerDebugViewMenuItem(MenuId.DebugWatchContext, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, 20, undefined, undefined, 'z_commands');
159161

0 commit comments

Comments
 (0)