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">
|
||||
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)" />
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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)})`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue