UNPKG

chrome-devtools-frontend

Version:
481 lines (420 loc) • 17.4 kB
// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import './LinearMemoryViewer.js'; import * as Common from '../../../core/common/common.js'; import * as i18n from '../../../core/i18n/i18n.js'; import * as UI from '../../../ui/legacy/legacy.js'; import {html, nothing, render} from '../../../ui/lit/lit.js'; import {LinearMemoryHighlightChipList} from './LinearMemoryHighlightChipList.js'; import linearMemoryInspectorStyles from './linearMemoryInspector.css.js'; import {formatAddress, parseAddress} from './LinearMemoryInspectorUtils.js'; import { type AddressInputChangedEvent, type HistoryNavigationEvent, Mode, Navigation, type PageNavigationEvent, } from './LinearMemoryNavigator.js'; import {LinearMemoryValueInterpreter} from './LinearMemoryValueInterpreter.js'; import type {ByteSelectedEvent, ResizeEvent} from './LinearMemoryViewer.js'; import type {HighlightInfo} from './LinearMemoryViewerUtils.js'; import { Endianness, getDefaultValueTypeMapping, VALUE_INTEPRETER_MAX_NUM_BYTES, type ValueType, type ValueTypeMode, } from './ValueInterpreterDisplayUtils.js'; const UIStrings = { /** * @description Tooltip text that appears when hovering over an invalid address in the address line in the Linear memory inspector * @example {0x00000000} PH1 * @example {0x00400000} PH2 */ addressHasToBeANumberBetweenSAnd: 'Address has to be a number between {PH1} and {PH2}', } as const; const str_ = i18n.i18n.registerUIStrings('panels/linear_memory_inspector/components/LinearMemoryInspector.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const {widgetConfig} = UI.Widget; /** * If the LinearMemoryInspector only receives a portion * of the original Uint8Array to show, it requires information * on the 1. memoryOffset (at which index this portion starts), * and on the 2. outerMemoryLength (length of the original Uint8Array). **/ export interface LinearMemoryInspectorData { memory: Uint8Array<ArrayBuffer>; address: number; memoryOffset: number; outerMemoryLength: number; valueTypes?: Set<ValueType>; valueTypeModes?: Map<ValueType, ValueTypeMode>; endianness?: Endianness; highlightInfo?: HighlightInfo; hideValueInspector?: boolean; } export interface Settings { valueTypes: Set<ValueType>; modes: Map<ValueType, ValueTypeMode>; endianness: Endianness; } export const enum Events { MEMORY_REQUEST = 'MemoryRequest', ADDRESS_CHANGED = 'AddressChanged', SETTINGS_CHANGED = 'SettingsChanged', DELETE_MEMORY_HIGHLIGHT = 'DeleteMemoryHighlight', } export interface EventTypes { [Events.MEMORY_REQUEST]: {start: number, end: number, address: number}; [Events.ADDRESS_CHANGED]: number; [Events.SETTINGS_CHANGED]: Settings; [Events.DELETE_MEMORY_HIGHLIGHT]: HighlightInfo; } class AddressHistoryEntry implements Common.SimpleHistoryManager.HistoryEntry { #address = 0; #callback; constructor(address: number, callback: (x: number) => void) { if (address < 0) { throw new Error('Address should be a greater or equal to zero'); } this.#address = address; this.#callback = callback; } valid(): boolean { return true; } reveal(): void { this.#callback(this.#address); } } export interface ViewInput { memory: Uint8Array; address: number; memoryOffset: number; outerMemoryLength: number; valueTypes: Set<ValueType>; valueTypeModes: Map<ValueType, ValueTypeMode>; endianness: Endianness; highlightInfo?: HighlightInfo; hideValueInspector: boolean; currentNavigatorMode: Mode; currentNavigatorAddressLine: string; canGoBackInHistory: boolean; canGoForwardInHistory: boolean; onRefreshRequest: () => void; onAddressChange: (e: AddressInputChangedEvent) => void; onNavigatePage: (e: PageNavigationEvent) => void; onNavigateHistory: (e: HistoryNavigationEvent) => boolean; onJumpToAddress: (address: number) => void; onDeleteMemoryHighlight: (info: HighlightInfo) => void; onByteSelected: (e: ByteSelectedEvent) => void; onResize: (e: ResizeEvent) => void; onValueTypeToggled: (type: ValueType, checked: boolean) => void; onValueTypeModeChanged: (type: ValueType, mode: ValueTypeMode) => void; onEndiannessChanged: (endianness: Endianness) => void; memorySlice: Uint8Array<ArrayBuffer>; viewerStart: number; } export const DEFAULT_VIEW = (input: ViewInput, _output: Record<string, unknown>, target: HTMLElement): void => { const navigatorAddressToShow = input.currentNavigatorMode === Mode.SUBMITTED ? formatAddress(input.address) : input.currentNavigatorAddressLine; const navigatorAddressIsValid = isValidAddress(navigatorAddressToShow, input.outerMemoryLength); const invalidAddressMsg = i18nString( UIStrings.addressHasToBeANumberBetweenSAnd, {PH1: formatAddress(0), PH2: formatAddress(input.outerMemoryLength)}); const errorMsg = navigatorAddressIsValid ? undefined : invalidAddressMsg; const highlightedMemoryAreas = input.highlightInfo ? [input.highlightInfo] : []; const focusedMemoryHighlight = getSmallestEnclosingMemoryHighlight(highlightedMemoryAreas, input.address); // Disabled until https://crbug.com/1079231 is fixed. // clang-format off render(html` <style>${linearMemoryInspectorStyles}</style> <div class="view"> <devtools-linear-memory-inspector-navigator .data=${ { address: navigatorAddressToShow, valid: navigatorAddressIsValid, mode: input.currentNavigatorMode, error: errorMsg, canGoBackInHistory: input.canGoBackInHistory, canGoForwardInHistory: input.canGoForwardInHistory, }} @refreshrequested=${input.onRefreshRequest} @addressinputchanged=${input.onAddressChange} @pagenavigation=${input.onNavigatePage} @historynavigation=${input.onNavigateHistory}></devtools-linear-memory-inspector-navigator> <devtools-widget .widgetConfig=${widgetConfig(LinearMemoryHighlightChipList, { highlightInfos: highlightedMemoryAreas, focusedMemoryHighlight, jumpToAddress: (address: number) => input.onJumpToAddress(address), deleteHighlight: input.onDeleteMemoryHighlight, })}> </devtools-widget> <devtools-linear-memory-inspector-viewer .data=${ { memory: input.memorySlice, address: input.address, memoryOffset: input.viewerStart, focus: input.currentNavigatorMode === Mode.SUBMITTED, highlightInfo: input.highlightInfo, focusedMemoryHighlight, }} @byteselected=${input.onByteSelected} @resize=${input.onResize}> </devtools-linear-memory-inspector-viewer> </div> ${ input.hideValueInspector ? nothing : html` <div class="value-interpreter"> <devtools-widget .widgetConfig=${widgetConfig(LinearMemoryValueInterpreter, { buffer: input.memory .slice( input.address - input.memoryOffset, input.address + VALUE_INTEPRETER_MAX_NUM_BYTES, ) .buffer, valueTypes: input.valueTypes, valueTypeModes: input.valueTypeModes, endianness: input.endianness, memoryLength: input.outerMemoryLength, onValueTypeModeChange: input.onValueTypeModeChanged, onJumpToAddressClicked: input.onJumpToAddress, onValueTypeToggled: input.onValueTypeToggled, onEndiannessChanged: input.onEndiannessChanged, })}></devtools-widget> </div>`} `, target); // clang-format on }; function getPageRangeForAddress( address: number, numBytesPerPage: number, outerMemoryLength: number): {start: number, end: number} { const pageNumber = Math.floor(address / numBytesPerPage); const pageStartAddress = pageNumber * numBytesPerPage; const pageEndAddress = Math.min(pageStartAddress + numBytesPerPage, outerMemoryLength); return {start: pageStartAddress, end: pageEndAddress}; } function isValidAddress(address: string, outerMemoryLength: number): boolean { const newAddress = parseAddress(address); return newAddress !== undefined && newAddress >= 0 && newAddress < outerMemoryLength; } // Returns the highlightInfo with the smallest size property that encloses the provided address. // If there are multiple smallest enclosing highlights, we pick the one appearing the earliest in highlightedMemoryAreas. // If no such highlightInfo exists, it returns undefined. // // Selecting the smallest enclosing memory highlight is a heuristic that aims to pick the // most specific highlight given a provided address. This way, objects contained in other objects are // potentially still accessible. function getSmallestEnclosingMemoryHighlight(highlightedMemoryAreas: HighlightInfo[], address: number): HighlightInfo| undefined { let smallestEnclosingHighlight; for (const highlightedMemory of highlightedMemoryAreas) { if (highlightedMemory.startAddress <= address && address < highlightedMemory.startAddress + highlightedMemory.size) { if (!smallestEnclosingHighlight) { smallestEnclosingHighlight = highlightedMemory; } else if (highlightedMemory.size < smallestEnclosingHighlight.size) { smallestEnclosingHighlight = highlightedMemory; } } } return smallestEnclosingHighlight; } export type View = typeof DEFAULT_VIEW; export class LinearMemoryInspector extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.Widget>( UI.Widget.Widget) { readonly #history = new Common.SimpleHistoryManager.SimpleHistoryManager(10); #memory = new Uint8Array(); #memoryOffset = 0; #outerMemoryLength = 0; #address = -1; #highlightInfo?: HighlightInfo; #currentNavigatorMode = Mode.SUBMITTED; #currentNavigatorAddressLine = `${this.#address}`; #numBytesPerPage = 4; #valueTypeModes = getDefaultValueTypeMapping(); #valueTypes = new Set(this.#valueTypeModes.keys()); #endianness = Endianness.LITTLE; #hideValueInspector = false; #view: View; constructor(element?: HTMLElement, view?: View) { super(element); this.#view = view ?? DEFAULT_VIEW; } set memory(value: Uint8Array<ArrayBuffer>) { this.#memory = value; void this.requestUpdate(); } set memoryOffset(value: number) { this.#memoryOffset = value; void this.requestUpdate(); } set outerMemoryLength(value: number) { this.#outerMemoryLength = value; void this.requestUpdate(); } set highlightInfo(value: HighlightInfo|undefined) { this.#highlightInfo = value; void this.requestUpdate(); } set valueTypeModes(value: Map<ValueType, ValueTypeMode>) { this.#valueTypeModes = value; void this.requestUpdate(); } set valueTypes(value: Set<ValueType>) { this.#valueTypes = value; void this.requestUpdate(); } set endianness(value: Endianness) { this.#endianness = value; void this.requestUpdate(); } set hideValueInspector(value: boolean) { this.#hideValueInspector = value; void this.requestUpdate(); } get hideValueInspector(): boolean { return this.#hideValueInspector; } override performUpdate(): void { const {start, end} = getPageRangeForAddress(this.#address, this.#numBytesPerPage, this.#outerMemoryLength); if (start < this.#memoryOffset || end > this.#memoryOffset + this.#memory.length) { this.dispatchEventToListeners(Events.MEMORY_REQUEST, {start, end, address: this.#address}); return; } if (this.#address < this.#memoryOffset || this.#address > this.#memoryOffset + this.#memory.length || this.#address < 0) { throw new Error('Address is out of bounds.'); } if (this.#highlightInfo) { if (this.#highlightInfo.size < 0) { this.#highlightInfo = undefined; throw new Error('Object size has to be greater than or equal to zero'); } if (this.#highlightInfo.startAddress < 0 || this.#highlightInfo.startAddress >= this.#outerMemoryLength) { this.#highlightInfo = undefined; throw new Error('Object start address is out of bounds.'); } } const viewInput: ViewInput = { memory: this.#memory, address: this.#address, memoryOffset: this.#memoryOffset, outerMemoryLength: this.#outerMemoryLength, valueTypes: this.#valueTypes, valueTypeModes: this.#valueTypeModes, endianness: this.#endianness, highlightInfo: this.#highlightInfo, hideValueInspector: this.#hideValueInspector, currentNavigatorMode: this.#currentNavigatorMode, currentNavigatorAddressLine: this.#currentNavigatorAddressLine, canGoBackInHistory: this.#history.canRollback(), canGoForwardInHistory: this.#history.canRollover(), onRefreshRequest: this.#onRefreshRequest.bind(this), onAddressChange: this.#onAddressChange.bind(this), onNavigatePage: this.#navigatePage.bind(this), onNavigateHistory: this.#navigateHistory.bind(this), onJumpToAddress: this.#onJumpToAddress.bind(this), onDeleteMemoryHighlight: this.#onDeleteMemoryHighlight.bind(this), onByteSelected: this.#onByteSelected.bind(this), onResize: this.#resize.bind(this), onValueTypeToggled: this.#onValueTypeToggled.bind(this), onValueTypeModeChanged: this.#onValueTypeModeChanged.bind(this), onEndiannessChanged: this.#onEndiannessChanged.bind(this), memorySlice: this.#memory.slice(start - this.#memoryOffset, end - this.#memoryOffset), viewerStart: start, }; this.#view(viewInput, {}, this.contentElement); } #onJumpToAddress(address: number): void { this.#currentNavigatorMode = Mode.SUBMITTED; const addressInRange = Math.max(0, Math.min(address, this.#outerMemoryLength - 1)); this.#jumpToAddress(addressInRange); } #onDeleteMemoryHighlight(highlight: HighlightInfo): void { this.dispatchEventToListeners(Events.DELETE_MEMORY_HIGHLIGHT, highlight); } #onRefreshRequest(): void { const {start, end} = getPageRangeForAddress(this.#address, this.#numBytesPerPage, this.#outerMemoryLength); this.dispatchEventToListeners(Events.MEMORY_REQUEST, {start, end, address: this.#address}); } #onByteSelected(e: ByteSelectedEvent): void { this.#currentNavigatorMode = Mode.SUBMITTED; const addressInRange = Math.max(0, Math.min(e.data, this.#outerMemoryLength - 1)); this.#jumpToAddress(addressInRange); } #createSettings(): Settings { return {valueTypes: this.#valueTypes, modes: this.#valueTypeModes, endianness: this.#endianness}; } #onEndiannessChanged(endianness: Endianness): void { this.#endianness = endianness; this.dispatchEventToListeners(Events.SETTINGS_CHANGED, this.#createSettings()); void this.requestUpdate(); } #onAddressChange(e: AddressInputChangedEvent): void { const {address, mode} = e.data; const isValid = isValidAddress(address, this.#outerMemoryLength); const newAddress = parseAddress(address); this.#currentNavigatorAddressLine = address; if (newAddress !== undefined && isValid) { this.#currentNavigatorMode = mode; this.#jumpToAddress(newAddress); return; } if (mode === Mode.SUBMITTED && !isValid) { this.#currentNavigatorMode = Mode.INVALID_SUBMIT; } else { this.#currentNavigatorMode = Mode.EDIT; } void this.requestUpdate(); } #onValueTypeToggled(type: ValueType, checked: boolean): void { if (checked) { this.#valueTypes.add(type); } else { this.#valueTypes.delete(type); } this.dispatchEventToListeners(Events.SETTINGS_CHANGED, this.#createSettings()); void this.requestUpdate(); } #onValueTypeModeChanged(type: ValueType, mode: ValueTypeMode): void { this.#valueTypeModes.set(type, mode); this.dispatchEventToListeners(Events.SETTINGS_CHANGED, this.#createSettings()); void this.requestUpdate(); } #navigateHistory(e: HistoryNavigationEvent): boolean { return e.data === Navigation.FORWARD ? this.#history.rollover() : this.#history.rollback(); } #navigatePage(e: PageNavigationEvent): void { const newAddress = e.data === Navigation.FORWARD ? this.#address + this.#numBytesPerPage : this.#address - this.#numBytesPerPage; const addressInRange = Math.max(0, Math.min(newAddress, this.#outerMemoryLength - 1)); this.#jumpToAddress(addressInRange); } #jumpToAddress(address: number): void { if (address < 0 || address >= this.#outerMemoryLength) { console.warn(`Specified address is out of bounds: ${address}`); return; } this.address = address; void this.requestUpdate(); } #resize(event: ResizeEvent): void { this.#numBytesPerPage = event.data; void this.requestUpdate(); } set address(address: number) { // If we are already showing the address that is requested, no need to act upon it. if (this.#address === address) { return; } const historyEntry = new AddressHistoryEntry(address, () => this.#jumpToAddress(address)); this.#history.push(historyEntry); this.#address = address; this.dispatchEventToListeners(Events.ADDRESS_CHANGED, this.#address); void this.requestUpdate(); } }