feat: add hex dump viewer, show appended SHA256, remove custom app desc

This commit is contained in:
kerms 2026-03-10 12:59:46 +01:00
parent 235b5e170a
commit 9b572bb9d3
Signed by: kerms
GPG Key ID: 5432C10DDCF8DAD5
5 changed files with 97 additions and 32 deletions

View File

@ -1,16 +1,19 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref } from 'vue';
import {
type AppImageInfo,
SPI_FLASH_MODE_NAMES, SPI_FLASH_SPEED_NAMES, SPI_FLASH_SIZE_NAMES,
parseAppImage,
} from '../../lib/app-image';
import HexDump from './HexDump.vue';
const props = defineProps<{
isDark?: boolean;
}>();
const imageInfo = ref<AppImageInfo | null>(null);
const rawData = ref<Uint8Array | null>(null);
const showHex = ref(false);
const statusMessage = ref('');
const statusType = ref<'success' | 'error' | 'info'>('info');
const fileName = ref('');
@ -34,10 +37,6 @@ function formatHex(val: number): string {
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 {
// Check if all zeros (not computed)
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 文件');
}
imageInfo.value = parseAppImage(data);
rawData.value = data;
showHex.value = false;
fileName.value = file.name;
showStatus(`已加载 ${file.name} (${data.byteLength} 字节)`, 'success');
} 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="最小芯片版本">{{ 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.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>
<!-- Segments -->
@ -134,13 +141,13 @@ async function handleOpenFile(file: File): Promise<false> {
</el-table-column>
</el-table>
<!-- Custom App Description (raw bytes) -->
<template v-if="imageInfo.customDescRawBytes && !imageInfo.customDescRawBytes.every(b => b === 0)">
<el-text tag="b" class="block mb-2">自定义应用描述偏移 288 B原始字节</el-text>
<el-text size="small" class="font-mono break-all">
{{ formatHexDump(imageInfo.customDescRawBytes) }}
</el-text>
</template>
<!-- Hex dump -->
<div class="mt-3">
<el-button size="small" @click="showHex = !showHex">
{{ showHex ? '隐藏原始字节' : '查看原始字节' }}
</el-button>
<HexDump v-if="showHex && rawData" :data="rawData" :height="400" class="mt-2" />
</div>
</template>
<el-empty v-else description="请打开一个ESP32固件文件 (.bin)" />

View File

@ -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(' ') }}&nbsp;&nbsp;{{ 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>

View File

@ -16,12 +16,6 @@ export const APP_DESC_MAGIC = 0xABCD5432;
/** Size of esp_app_desc_t structure */
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 */
export const CHIP_ID_NAMES: Record<number, string> = {
0x0000: 'ESP32',

View File

@ -5,7 +5,6 @@ import type {
import {
IMAGE_MAGIC, IMAGE_HEADER_SIZE, EXTENDED_HEADER_SIZE,
SEGMENT_HEADER_SIZE, APP_DESC_MAGIC, APP_DESC_SIZE, CHIP_ID_NAMES,
CUSTOM_DESC_OFFSET_IN_SEGMENT, CUSTOM_DESC_DUMP_SIZE,
} 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 {
header,
extendedHeader,
segments,
appDescription,
customDescRawBytes,
valid: segments.length === segmentCount, // false if image was truncated mid-segment
chipName: CHIP_ID_NAMES[chipId] ?? `Unknown (0x${chipId.toString(16)})`,
};

View File

@ -63,8 +63,6 @@ export interface AppImageInfo {
extendedHeader: ExtendedHeader;
segments: SegmentHeader[];
appDescription: AppDescription | null;
/** Raw bytes at the custom app desc location (null if first segment too short) */
customDescRawBytes: Uint8Array | null;
valid: boolean;
chipName: string;
}