binhop/static/binhop.js

305 lines
10 KiB
JavaScript

function draw_bytes(data) {
const canvas = document.getElementById("blob");
const ctx = canvas.getContext("2d");
const colorScale = chroma.scale(['#4c6c81', '#9c2f2f']).mode('lch');
const numBlocks = Math.ceil(data.meta.sizeb / 1200);
const blockSize = canvas.width / numBlocks;
ctx.fillStyle = "#ddd";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// console.log("size: " + data.meta.sizeb);
// console.log("numblocks: " + numBlocks);
data.offsets.forEach((offset, idx) => {
const start = Math.floor((offset.start / data.meta.sizeb) * numBlocks);
const end = Math.ceil((offset.end / data.meta.sizeb) * numBlocks);
// console.log(offset.d + ": " + start + ", " + end + " | " + offset.start + ", " + offset.end);
ctx.fillStyle = colorScale(idx / data.offsets.length).alpha(0.2);
ctx.fillRect(start * blockSize, 0, (end - start) * blockSize, canvas.height);
});
canvas.addEventListener('mousemove', function(event) {
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const tooltips = [];
data.offsets.forEach((offset, idx) => {
const start = Math.floor((offset.start / data.meta.sizeb) * numBlocks);
const end = Math.ceil((offset.end / data.meta.sizeb) * numBlocks);
const rectX = start * blockSize;
const rectY = 0;
const rectWidth = (end - start) * blockSize;
const rectHeight = canvas.height;
if (mouseX >= rectX && mouseX <= rectX + rectWidth && mouseY >= rectY && mouseY <= rectY + rectHeight) {
const tooltipText = ` ${first_substr(offset.d)}`;
const tooltipWidth = ctx.measureText(tooltipText).width + 20;
const tooltipHeight = 30;
let tooltipX = mouseX + 10;
let tooltipY = mouseY - tooltipHeight - 10;
if (tooltipX + tooltipWidth > canvas.width) {
tooltipX = canvas.width - tooltipWidth;
}
if (tooltipY < 0) {
tooltipY = 0;
}
// Check for overlapping tooltips
for (let i = 0; i < tooltips.length; i++) {
const otherTooltip = tooltips[i];
if (tooltipX < otherTooltip.x + otherTooltip.width &&
tooltipX + tooltipWidth > otherTooltip.x &&
tooltipY < otherTooltip.y + otherTooltip.height &&
tooltipY + tooltipHeight > otherTooltip.y) {
// Overlapping, adjust the position
tooltipX = otherTooltip.x + otherTooltip.width + 10;
if (tooltipX + tooltipWidth > canvas.width) {
tooltipX = mouseX - tooltipWidth - 10;
}
if (tooltipX < 0) {
tooltipX = 0;
}
tooltipY = otherTooltip.y;
}
}
tooltips.push({
x: tooltipX,
y: tooltipY,
width: tooltipWidth,
height: tooltipHeight
});
ctx.fillStyle = '#fff';
ctx.fillRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight);
ctx.fillStyle = '#000';
ctx.font = '.6em Arial';
ctx.fillText(tooltipText, tooltipX + 10, tooltipY + 20);
// colored circle
const circleSize = 10;
ctx.fillStyle = colorScale(idx / data.offsets.length).alpha(0.2);
ctx.beginPath();
ctx.arc(tooltipX + 15, tooltipY + 15, circleSize / 2, 0, 2 * Math.PI);
ctx.fill();
// re-draw the rest
ctx.fillStyle = colorScale(idx / data.offsets.length).alpha(0.2);
ctx.fillRect(start * blockSize, 0, (end - start) * blockSize, canvas.height);
}
});
});
canvas.addEventListener('mouseout', function(event) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
data.offsets.forEach((offset, idx) => {
const start = Math.floor((offset.start / data.meta.sizeb) * numBlocks);
const end = Math.ceil((offset.end / data.meta.sizeb) * numBlocks);
ctx.fillStyle = colorScale(idx / data.offsets.length).alpha(0.2);
ctx.fillRect(start * blockSize, 0, (end - start) * blockSize, canvas.height);
});
});
}
// function create_listing(listing) {
// const ul = document.createElement('ul');
// ul.classList.add("tree-padding", "tree-vertical-lines", "tree-horizontal-lines", "tree-summaries", "tree-markers", "tree-buttons")
// for (const key in listing) {
// const value = listing[key];
// if (typeof value == 'object') {
// const li = document.createElement('li');
// const summary = document.createElement('summary');
// summary.innerText = key;
// li.appendChild(summary);
// const nestedUl = create_listing(value);
// li.appendChild(nestedUl);
// ul.appendChild(li);
// } else {
// const li = document.createElement('li');
// const summary = document.createElement('summary');
// summary.classList.add('no-pico');
// summary.innerText = `${key} (${value.s})`;
// li.appendChild(summary);
// ul.appendChild(li);
// }
// }
// return ul;
// }
function file_summaries(data) {
const ul = document.createElement('ul');
for (const [key, val] of Object.entries(data.ql)) {
const li = document.createElement('li');
li.textContent = `${val}`;
ul.appendChild(li);
}
return ul;
}
function create_listing(listing) {
const ul = document.createElement('ul');
for (const key in listing) {
const value = listing[key];
if ('s' in value) {
const li = document.createElement('li');
li.innerText = `${key} (${bytes_to_human(value.s)})`;
ul.appendChild(li);
}
else { // (typeof value === 'object') {
const li = document.createElement('li');
li.innerText = `${key}/`;
const nestedUl = create_listing(value);
li.appendChild(nestedUl);
ul.appendChild(li);
}
}
return ul;
}
function bytes_to_human(bytes) {
num = parseInt(bytes);
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let i =0;
while (num >= 1024 && i < units.length - 1) {
num /= 1024;
i++;
}
return num.toFixed(2) + " " + units[i];
}
function first_substr(description) {
const index = description.indexOf(',');
if (index === -1) {
return description;
} else {
return description.substring(0, index);
}
}
async function process_upload(file) {
const formData = new FormData();
formData.append('file', file);
const button = document.querySelector('button');
button.setAttribute('aria-busy', 'true');
button.textContent = `processing ${file.name}...`;
const response = await fetch('api/upload_file', {
method: 'POST',
body: formData,
timeout: 600000 // 10 minutes
});
if (response.ok) {
// if (1 === 1) {
const data = await response.json();
// const json = '{"meta":{"name":"miwifi_r4av2_firmware_6bdd4_2.30.500.bin","sizeb":13632488,"sha1":"6cefd9d5de3b80799d0341b19043d108624dcaca","md5":"639616a5b70983903ccdd019a7a6bdd4","sig_quant":411,"duration":"00:05.457"},"offsets":[{"start":960,"end":13630770,"d":"LZMA compressed data"},{"start":1704660,"end":13621866,"d":"Squashfs filesystem"},{"start":4534008,"end":5267984,"d":"xz compressed data"},{"start":5258380,"end":5267984,"d":"ASCII cpio archive"}]}';
// const data = JSON.parse(json);
const container = document.querySelector('.container');
// visualization
container.innerHTML = `
<div id="blob-row">
<canvas id="blob" width="1200px" height="60px" style="background-color: #18232c;"></canvas>
</div>
<div id="info-row">
<h4 style="margin-bottom:0.5em;">${data.meta.name}</h4>
<details open>
<summary>blob meta</summary>
<figure>
<table role="grid">
<tr>
<th><strong>input filename:</strong></th>
<td>${data.meta.name}</td>
</tr>
<tr>
<th><strong>process duration:</strong></th>
<td>${data.meta.duration}</td>
</tr>
<tr>
<th><strong>file size in bytes:</strong></th>
<td>${data.meta.sizeb} (${bytes_to_human(data.meta.sizeb)})</td>
</tr>
<tr>
<th><strong>files discovered:</strong></th>
<td>${data.meta.files}</td>
</tr>
<tr>
<th><strong>directories unpacked:</strong></th>
<td>${data.meta.dirs}</td>
</tr>
<tr>
<th><strong>signatures tried:</strong></th>
<td>${data.meta.sig_quant}</td>
</tr>
<tr>
<th><strong>sha1 hash:</strong></th>
<td>${data.meta.sha1}</td>
</tr>
<tr>
<th><strong>md5 hash:</strong></th>
<td>${data.meta.md5}</td>
</tr>
</table>
</figure>
</details>
<details open>
<summary>binwalk extraction log</summary>
<small>
<div id="quick-look"></div>
</small>
</details>
<details>
<summary>full file listing</summary>
<small>
<div id="file-list"></div>
</small>
</details>
<details>
<summary>raw json response (dev)</summary>
<pre>${JSON.stringify(data, null, 2)}</pre>
</details>
</div>`;
draw_bytes(data);
const file_list = document.getElementById('file-list');
file_list.appendChild(create_listing(data.ls[Object.keys(data.ls)[0]]));
const quick_look = document.getElementById('quick-look');
quick_look.appendChild(file_summaries(data));
} else {
console.error('error uploading file');
}
}
function upload_file() {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
process_upload(file);
});
fileInput.click();
}