feat: add hex dump viewer, show appended SHA256, remove custom app desc
This commit is contained in:
parent
235b5e170a
commit
9b572bb9d3
|
|
@ -1,16 +1,19 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref } from 'vue';
|
||||||
import {
|
import {
|
||||||
type AppImageInfo,
|
type AppImageInfo,
|
||||||
SPI_FLASH_MODE_NAMES, SPI_FLASH_SPEED_NAMES, SPI_FLASH_SIZE_NAMES,
|
SPI_FLASH_MODE_NAMES, SPI_FLASH_SPEED_NAMES, SPI_FLASH_SIZE_NAMES,
|
||||||
parseAppImage,
|
parseAppImage,
|
||||||
} from '../../lib/app-image';
|
} from '../../lib/app-image';
|
||||||
|
import HexDump from './HexDump.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isDark?: boolean;
|
isDark?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const imageInfo = ref<AppImageInfo | null>(null);
|
const imageInfo = ref<AppImageInfo | null>(null);
|
||||||
|
const rawData = ref<Uint8Array | null>(null);
|
||||||
|
const showHex = ref(false);
|
||||||
const statusMessage = ref('');
|
const statusMessage = ref('');
|
||||||
const statusType = ref<'success' | 'error' | 'info'>('info');
|
const statusType = ref<'success' | 'error' | 'info'>('info');
|
||||||
const fileName = ref('');
|
const fileName = ref('');
|
||||||
|
|
@ -34,10 +37,6 @@ function formatHex(val: number): string {
|
||||||
return '0x' + val.toString(16).toUpperCase();
|
return '0x' + val.toString(16).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHexDump(data: Uint8Array): string {
|
|
||||||
return Array.from(data).map(b => b.toString(16).padStart(2, '0')).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSha256(data: Uint8Array): string {
|
function formatSha256(data: Uint8Array): string {
|
||||||
// Check if all zeros (not computed)
|
// Check if all zeros (not computed)
|
||||||
if (data.every(b => b === 0)) return '(未计算)';
|
if (data.every(b => b === 0)) return '(未计算)';
|
||||||
|
|
@ -53,6 +52,8 @@ async function handleOpenFile(file: File): Promise<false> {
|
||||||
throw new Error('ELF 格式不支持。请使用 esptool.py elf2image 将其转换为 .bin 文件');
|
throw new Error('ELF 格式不支持。请使用 esptool.py elf2image 将其转换为 .bin 文件');
|
||||||
}
|
}
|
||||||
imageInfo.value = parseAppImage(data);
|
imageInfo.value = parseAppImage(data);
|
||||||
|
rawData.value = data;
|
||||||
|
showHex.value = false;
|
||||||
fileName.value = file.name;
|
fileName.value = file.name;
|
||||||
showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success');
|
showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success');
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -117,7 +118,13 @@ async function handleOpenFile(file: File): Promise<false> {
|
||||||
<el-descriptions-item label="SPI引脚驱动">{{ imageInfo.extendedHeader.spiPinDrv.map(formatHex).join(' / ') }}</el-descriptions-item>
|
<el-descriptions-item label="SPI引脚驱动">{{ imageInfo.extendedHeader.spiPinDrv.map(formatHex).join(' / ') }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最小芯片版本">{{ imageInfo.extendedHeader.minChipRevFull / 100 }}</el-descriptions-item>
|
<el-descriptions-item label="最小芯片版本">{{ imageInfo.extendedHeader.minChipRevFull / 100 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最大芯片版本">{{ imageInfo.extendedHeader.maxChipRevFull === 0xFFFF ? '不限' : imageInfo.extendedHeader.maxChipRevFull / 100 }}</el-descriptions-item>
|
<el-descriptions-item label="最大芯片版本">{{ imageInfo.extendedHeader.maxChipRevFull === 0xFFFF ? '不限' : imageInfo.extendedHeader.maxChipRevFull / 100 }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="附加哈希">{{ imageInfo.extendedHeader.hashAppended ? '是' : '否' }}</el-descriptions-item>
|
<el-descriptions-item label="附加哈希" :span="imageInfo.extendedHeader.hashAppended ? 2 : 1">
|
||||||
|
{{ imageInfo.extendedHeader.hashAppended ? '是' : '否' }}
|
||||||
|
<el-text v-if="imageInfo.extendedHeader.hashAppended && rawData"
|
||||||
|
size="small" class="font-mono" style="margin-left:8px">
|
||||||
|
{{ formatSha256(rawData.slice(-32)) }}
|
||||||
|
</el-text>
|
||||||
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<!-- Segments -->
|
<!-- Segments -->
|
||||||
|
|
@ -134,13 +141,13 @@ async function handleOpenFile(file: File): Promise<false> {
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- Custom App Description (raw bytes) -->
|
<!-- Hex dump -->
|
||||||
<template v-if="imageInfo.customDescRawBytes && !imageInfo.customDescRawBytes.every(b => b === 0)">
|
<div class="mt-3">
|
||||||
<el-text tag="b" class="block mb-2">自定义应用描述(偏移 288 B,原始字节)</el-text>
|
<el-button size="small" @click="showHex = !showHex">
|
||||||
<el-text size="small" class="font-mono break-all">
|
{{ showHex ? '隐藏原始字节' : '查看原始字节' }}
|
||||||
{{ formatHexDump(imageInfo.customDescRawBytes) }}
|
</el-button>
|
||||||
</el-text>
|
<HexDump v-if="showHex && rawData" :data="rawData" :height="400" class="mt-2" />
|
||||||
</template>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-empty v-else description="请打开一个ESP32固件文件 (.bin)" />
|
<el-empty v-else description="请打开一个ESP32固件文件 (.bin)" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ data: Uint8Array; height?: number }>(), { height: 400 })
|
||||||
|
|
||||||
|
const BYTES = 16
|
||||||
|
const ROW_H = 20
|
||||||
|
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
|
||||||
|
const totalRows = computed(() => Math.ceil(props.data.length / BYTES))
|
||||||
|
const firstRow = computed(() => Math.floor(scrollTop.value / ROW_H))
|
||||||
|
const lastRow = computed(() => Math.min(firstRow.value + Math.ceil(props.height / ROW_H) + 4, totalRows.value))
|
||||||
|
|
||||||
|
const rows = computed(() => {
|
||||||
|
const out = []
|
||||||
|
for (let r = firstRow.value; r < lastRow.value; r++) {
|
||||||
|
const off = r * BYTES
|
||||||
|
const slice = props.data.subarray(off, off + BYTES)
|
||||||
|
const hex = Array.from(slice).map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
const asc = Array.from(slice).map(b => (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.')
|
||||||
|
out.push({ off, hex, asc })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
function onScroll(e: Event) {
|
||||||
|
scrollTop.value = (e.target as HTMLElement).scrollTop
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="hd-wrap" :style="{ height: height + 'px' }" @scroll="onScroll">
|
||||||
|
<div :style="{ height: totalRows * ROW_H + 'px', position: 'relative' }">
|
||||||
|
<div :style="{ transform: `translateY(${firstRow * ROW_H}px)` }">
|
||||||
|
<div v-for="row in rows" :key="row.off" class="hd-row">
|
||||||
|
<span class="hd-off">{{ row.off.toString(16).padStart(8, '0') }}</span>
|
||||||
|
<span class="hd-hex">{{ row.hex.slice(0, 8).join(' ') }} {{ row.hex.slice(8).join(' ') }}</span>
|
||||||
|
<span class="hd-asc">|{{ row.asc.join('') }}|</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hd-wrap {
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--el-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--el-fill-color-blank);
|
||||||
|
}
|
||||||
|
.hd-row {
|
||||||
|
display: flex;
|
||||||
|
height: 20px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
.hd-row:hover {
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
}
|
||||||
|
.hd-off {
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
min-width: 6em;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.hd-hex {
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
.hd-asc {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -16,12 +16,6 @@ export const APP_DESC_MAGIC = 0xABCD5432;
|
||||||
/** Size of esp_app_desc_t structure */
|
/** Size of esp_app_desc_t structure */
|
||||||
export const APP_DESC_SIZE = 256;
|
export const APP_DESC_SIZE = 256;
|
||||||
|
|
||||||
/** Offset of custom app desc within first segment data (immediately after esp_app_desc_t) */
|
|
||||||
export const CUSTOM_DESC_OFFSET_IN_SEGMENT = APP_DESC_SIZE; // 256
|
|
||||||
|
|
||||||
/** How many raw bytes to extract for the custom app desc dump */
|
|
||||||
export const CUSTOM_DESC_DUMP_SIZE = 64;
|
|
||||||
|
|
||||||
/** Chip ID to human-readable name */
|
/** Chip ID to human-readable name */
|
||||||
export const CHIP_ID_NAMES: Record<number, string> = {
|
export const CHIP_ID_NAMES: Record<number, string> = {
|
||||||
0x0000: 'ESP32',
|
0x0000: 'ESP32',
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import type {
|
||||||
import {
|
import {
|
||||||
IMAGE_MAGIC, IMAGE_HEADER_SIZE, EXTENDED_HEADER_SIZE,
|
IMAGE_MAGIC, IMAGE_HEADER_SIZE, EXTENDED_HEADER_SIZE,
|
||||||
SEGMENT_HEADER_SIZE, APP_DESC_MAGIC, APP_DESC_SIZE, CHIP_ID_NAMES,
|
SEGMENT_HEADER_SIZE, APP_DESC_MAGIC, APP_DESC_SIZE, CHIP_ID_NAMES,
|
||||||
CUSTOM_DESC_OFFSET_IN_SEGMENT, CUSTOM_DESC_DUMP_SIZE,
|
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,21 +93,11 @@ export function parseAppImage(data: Uint8Array): AppImageInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Custom App Description — fixed offset in first segment ──
|
|
||||||
let customDescRawBytes: Uint8Array | null = null;
|
|
||||||
if (segDataOffsets.length > 0) {
|
|
||||||
const customOff = segDataOffsets[0] + CUSTOM_DESC_OFFSET_IN_SEGMENT;
|
|
||||||
if (customOff + CUSTOM_DESC_DUMP_SIZE <= data.length) {
|
|
||||||
customDescRawBytes = new Uint8Array(data.subarray(customOff, customOff + CUSTOM_DESC_DUMP_SIZE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
header,
|
header,
|
||||||
extendedHeader,
|
extendedHeader,
|
||||||
segments,
|
segments,
|
||||||
appDescription,
|
appDescription,
|
||||||
customDescRawBytes,
|
|
||||||
valid: segments.length === segmentCount, // false if image was truncated mid-segment
|
valid: segments.length === segmentCount, // false if image was truncated mid-segment
|
||||||
chipName: CHIP_ID_NAMES[chipId] ?? `Unknown (0x${chipId.toString(16)})`,
|
chipName: CHIP_ID_NAMES[chipId] ?? `Unknown (0x${chipId.toString(16)})`,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,6 @@ export interface AppImageInfo {
|
||||||
extendedHeader: ExtendedHeader;
|
extendedHeader: ExtendedHeader;
|
||||||
segments: SegmentHeader[];
|
segments: SegmentHeader[];
|
||||||
appDescription: AppDescription | null;
|
appDescription: AppDescription | null;
|
||||||
/** Raw bytes at the custom app desc location (null if first segment too short) */
|
|
||||||
customDescRawBytes: Uint8Array | null;
|
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
chipName: string;
|
chipName: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue