mirror of
https://github.com/NohamR/LLDBMemView.git
synced 2026-02-21 18:15:42 +00:00
Initial commit: LLDB Memory Region Parser web app
This commit is contained in:
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# LLDB Memory Region Parser
|
||||||
|
|
||||||
|
A web-based visualization tool for analyzing and parsing LLDB memory region output.
|
||||||
|
|
||||||
|
> **Note:** This project was vibecoded and built in a rush to support another debugging task.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
When debugging with LLDB, the `memory region --all` command outputs detailed information about process memory regions. This tool parses that raw output and presents it in a clean, interactive table with filtering, sorting, and statistics.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In LLDB
|
||||||
|
(lldb) process attach --pid <pid>
|
||||||
|
(lldb) memory region --all
|
||||||
|
```
|
||||||
|
Copy the output that looks like:
|
||||||
|
```
|
||||||
|
[0x0000000100000000-0x0000000100004000) r-x __TEXT
|
||||||
|
[0x0000000100004000-0x0000000100008000) rw- __DATA
|
||||||
|
Dirty pages: 0x100004000, 0x100005000
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Then paste it into the input area of this tool to visualize the memory regions.
|
||||||
200
index.html
Normal file
200
index.html
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="light">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LLDB Memory Region Parser</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
slate: { 850: '#1e293b' } // Custom dark shade
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body
|
||||||
|
class="bg-slate-100 dark:bg-slate-900 min-h-screen p-4 md:p-8 text-slate-800 dark:text-slate-200 transition-colors">
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto space-y-6">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-r from-slate-800 to-indigo-900 dark:from-slate-950 dark:to-indigo-950 rounded-xl shadow-lg p-8 text-white text-center relative">
|
||||||
|
<!-- Dark Mode Toggle -->
|
||||||
|
<button onclick="toggleDarkMode()"
|
||||||
|
class="absolute top-4 right-4 p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
|
||||||
|
title="Toggle dark mode">
|
||||||
|
<svg id="theme-icon-sun" class="h-6 w-6 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg id="theme-icon-moon" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h1 class="text-3xl font-bold mb-2 flex items-center justify-center gap-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-indigo-300" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
|
LLDB Memory Region Parser
|
||||||
|
</h1>
|
||||||
|
<p class="text-indigo-200 opacity-90">Analyze memory layout, permissions, and dirty pages from LLDB output
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Section -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-md overflow-hidden transition-colors">
|
||||||
|
<div class="p-6 border-b border-slate-100 dark:border-slate-700">
|
||||||
|
<label for="input" class="block text-sm font-semibold text-slate-600 dark:text-slate-300 mb-2">Paste
|
||||||
|
'memory region --all'
|
||||||
|
output:</label>
|
||||||
|
<textarea id="input"
|
||||||
|
class="w-full h-48 p-4 font-mono text-xs bg-slate-50 dark:bg-slate-900 border border-slate-200 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all resize-y text-slate-800 dark:text-slate-200"
|
||||||
|
placeholder="[0x0000000100000000-0x0000000100004000) r-x __TEXT..."></textarea>
|
||||||
|
|
||||||
|
<div class="mt-4 flex gap-3">
|
||||||
|
<button onclick="parseMemory()"
|
||||||
|
class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2.5 rounded-lg font-medium transition shadow-sm active:transform active:scale-95">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
Parse Memory
|
||||||
|
</button>
|
||||||
|
<button onclick="clearAll()"
|
||||||
|
class="flex items-center gap-2 bg-white dark:bg-slate-700 border border-slate-300 dark:border-slate-600 hover:bg-slate-50 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-200 px-6 py-2.5 rounded-lg font-medium transition shadow-sm">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Dashboard -->
|
||||||
|
<div id="stats"
|
||||||
|
class="hidden grid-cols-2 md:grid-cols-5 gap-4 p-6 bg-slate-50 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-700">
|
||||||
|
<!-- Stat Cards injected by JS -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div id="filter-section"
|
||||||
|
class="hidden p-6 bg-white dark:bg-slate-800 border-b border-slate-100 dark:border-slate-700 flex flex-wrap gap-4 items-center">
|
||||||
|
<div class="flex items-center gap-2 text-slate-600 dark:text-slate-300 font-medium">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||||
|
</svg>
|
||||||
|
Filters:
|
||||||
|
</div>
|
||||||
|
<input type="text" id="addressFilter" placeholder="Search Address or Name..."
|
||||||
|
class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-md text-sm focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 w-48 md:w-64 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200"
|
||||||
|
onkeyup="applyFilters()">
|
||||||
|
|
||||||
|
<select id="permsFilter" onchange="applyFilters()"
|
||||||
|
class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-md text-sm focus:ring-1 focus:ring-indigo-500 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200">
|
||||||
|
<option value="">Any Permission</option>
|
||||||
|
<option value="r">Readable (R)</option>
|
||||||
|
<option value="w">Writable (W)</option>
|
||||||
|
<option value="x">Executable (X)</option>
|
||||||
|
<option value="rw">Read & Write</option>
|
||||||
|
<option value="rx">Read & Exec</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="dirtyFilter" onchange="applyFilters()"
|
||||||
|
class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-md text-sm focus:ring-1 focus:ring-indigo-500 bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-200">
|
||||||
|
<option value="">All Pages</option>
|
||||||
|
<option value="dirty">Dirty Only</option>
|
||||||
|
<option value="clean">Clean Only</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="flex items-center gap-2 cursor-pointer select-none text-sm text-slate-700 dark:text-slate-200 bg-slate-100 dark:bg-slate-700 px-3 py-2 rounded-md hover:bg-slate-200 dark:hover:bg-slate-600 transition">
|
||||||
|
<input type="checkbox" id="hideEmptyRegions" onchange="applyFilters()" checked
|
||||||
|
class="rounded text-indigo-600 focus:ring-indigo-500">
|
||||||
|
<span>Hide "---" Regions</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="overflow-x-auto custom-scrollbar">
|
||||||
|
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700">
|
||||||
|
<thead class="bg-slate-50 dark:bg-slate-800 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('start')">
|
||||||
|
Start Address
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('end')">
|
||||||
|
End Address
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('size')">
|
||||||
|
Size
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('perms')">
|
||||||
|
Perms
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('name')">
|
||||||
|
Name / Details
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
<th scope="col"
|
||||||
|
class="px-6 py-3 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-700 group"
|
||||||
|
onclick="sortBy('dirty')">
|
||||||
|
Dirty
|
||||||
|
<span class="sort-icon invisible group-hover:visible ml-1 inline-block">⇅</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="table-body"
|
||||||
|
class="bg-white dark:bg-slate-800 divide-y divide-slate-200 dark:divide-slate-700">
|
||||||
|
<!-- Content injected by JS -->
|
||||||
|
<tr class="text-center text-slate-400">
|
||||||
|
<td colspan="6" class="py-12">Waiting for input...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footer-count"
|
||||||
|
class="bg-slate-50 dark:bg-slate-800/50 p-3 text-xs text-right text-slate-400 dark:text-slate-500 border-t border-slate-200 dark:border-slate-700">
|
||||||
|
0 rows
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="src.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
360
src.js
Normal file
360
src.js
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
let allRegions = [];
|
||||||
|
let currentSort = { column: null, direction: "asc" };
|
||||||
|
|
||||||
|
// Dark Mode Toggle
|
||||||
|
function toggleDarkMode() {
|
||||||
|
const html = document.documentElement;
|
||||||
|
const sunIcon = document.getElementById("theme-icon-sun");
|
||||||
|
const moonIcon = document.getElementById("theme-icon-moon");
|
||||||
|
|
||||||
|
if (html.classList.contains("dark")) {
|
||||||
|
html.classList.remove("dark");
|
||||||
|
html.classList.add("light");
|
||||||
|
sunIcon.classList.add("hidden");
|
||||||
|
moonIcon.classList.remove("hidden");
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
} else {
|
||||||
|
html.classList.remove("light");
|
||||||
|
html.classList.add("dark");
|
||||||
|
sunIcon.classList.remove("hidden");
|
||||||
|
moonIcon.classList.add("hidden");
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize theme from localStorage
|
||||||
|
(function initTheme() {
|
||||||
|
const savedTheme = localStorage.getItem("theme") || "light";
|
||||||
|
const html = document.documentElement;
|
||||||
|
const sunIcon = document.getElementById("theme-icon-sun");
|
||||||
|
const moonIcon = document.getElementById("theme-icon-moon");
|
||||||
|
|
||||||
|
if (savedTheme === "dark") {
|
||||||
|
html.classList.remove("light");
|
||||||
|
html.classList.add("dark");
|
||||||
|
if (sunIcon) sunIcon.classList.remove("hidden");
|
||||||
|
if (moonIcon) moonIcon.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
html.classList.remove("dark");
|
||||||
|
html.classList.add("light");
|
||||||
|
if (sunIcon) sunIcon.classList.add("hidden");
|
||||||
|
if (moonIcon) moonIcon.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Parse Logic
|
||||||
|
function parseMemory() {
|
||||||
|
const input = document.getElementById("input").value;
|
||||||
|
if (!input.trim()) return;
|
||||||
|
|
||||||
|
const lines = input.split("\n");
|
||||||
|
const regions = [];
|
||||||
|
let currentRegion = null;
|
||||||
|
|
||||||
|
// Updated Regex to be more robust
|
||||||
|
// Captures: 1: Start, 2: End, 3: Perms, 4: Name (optional)
|
||||||
|
const regionRegex =
|
||||||
|
/\[(0x[0-9a-fA-F]+)-(0x[0-9a-fA-F]+)\)\s+([r\-][w\-][x\-][s\-]?)(?:\s+(.*))?/i;
|
||||||
|
const dirtyRegex = /Dirty pages:\s+(.+)/i;
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
const regionMatch = line.match(regionRegex);
|
||||||
|
if (regionMatch) {
|
||||||
|
const start = regionMatch[1];
|
||||||
|
const end = regionMatch[2];
|
||||||
|
const perms = regionMatch[3];
|
||||||
|
const name = regionMatch[4] || "";
|
||||||
|
|
||||||
|
currentRegion = {
|
||||||
|
startStr: start, // Keep string for display
|
||||||
|
endStr: end, // Keep string for display
|
||||||
|
startBig: BigInt(start), // Use BigInt for calculation
|
||||||
|
endBig: BigInt(end), // Use BigInt for calculation
|
||||||
|
perms,
|
||||||
|
name,
|
||||||
|
dirtyPages: [],
|
||||||
|
dirtyCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate size safely using BigInt
|
||||||
|
currentRegion.size = currentRegion.endBig - currentRegion.startBig;
|
||||||
|
|
||||||
|
regions.push(currentRegion);
|
||||||
|
} else if (currentRegion) {
|
||||||
|
// Check for metadata lines associated with the previous region
|
||||||
|
const dirtyMatch = line.match(dirtyRegex);
|
||||||
|
if (dirtyMatch) {
|
||||||
|
const pages = dirtyMatch[1].split(",").map((p) => p.trim());
|
||||||
|
currentRegion.dirtyPages = pages;
|
||||||
|
currentRegion.dirtyCount = pages.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allRegions = regions;
|
||||||
|
updateStats(regions);
|
||||||
|
document.getElementById("stats").classList.remove("hidden");
|
||||||
|
document.getElementById("stats").classList.add("grid");
|
||||||
|
document.getElementById("filter-section").classList.remove("hidden");
|
||||||
|
document.getElementById("filter-section").classList.add("flex");
|
||||||
|
|
||||||
|
// Set default sort to Start Address Ascending
|
||||||
|
sortBy("start");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats(regions) {
|
||||||
|
const container = document.getElementById("stats");
|
||||||
|
|
||||||
|
const totalRegions = regions.length;
|
||||||
|
const totalDirty = regions.reduce((acc, r) => acc + r.dirtyCount, 0);
|
||||||
|
|
||||||
|
// BigInt total size summation
|
||||||
|
let totalSizeBytes = 0n;
|
||||||
|
regions.forEach((r) => (totalSizeBytes += r.size));
|
||||||
|
|
||||||
|
const execCount = regions.filter((r) => r.perms.includes("x")).length;
|
||||||
|
const writeCount = regions.filter((r) => r.perms.includes("w")).length;
|
||||||
|
|
||||||
|
const createCard = (label, value, colorClass) => `
|
||||||
|
<div class="bg-white dark:bg-slate-700 p-4 rounded-lg border border-slate-200 dark:border-slate-600 shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div class="text-xs font-bold text-slate-400 dark:text-slate-500 uppercase tracking-wider mb-1">${label}</div>
|
||||||
|
<div class="text-xl md:text-2xl font-bold ${colorClass}">${value}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.innerHTML =
|
||||||
|
createCard(
|
||||||
|
"Total Regions",
|
||||||
|
totalRegions,
|
||||||
|
"text-slate-800 dark:text-slate-200",
|
||||||
|
) +
|
||||||
|
createCard(
|
||||||
|
"Total Size",
|
||||||
|
formatBytes(totalSizeBytes),
|
||||||
|
"text-indigo-600 dark:text-indigo-400",
|
||||||
|
) +
|
||||||
|
createCard("Executable", execCount, "text-red-500 dark:text-red-400") +
|
||||||
|
createCard("Writable", writeCount, "text-amber-500 dark:text-amber-400") +
|
||||||
|
createCard(
|
||||||
|
"Dirty Pages",
|
||||||
|
totalDirty,
|
||||||
|
"text-emerald-600 dark:text-emerald-400",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(regions) {
|
||||||
|
const tbody = document.getElementById("table-body");
|
||||||
|
const footer = document.getElementById("footer-count");
|
||||||
|
|
||||||
|
if (regions.length === 0) {
|
||||||
|
tbody.innerHTML = `<tr><td colspan="6" class="py-8 text-center text-slate-400">No matching regions found.</td></tr>`;
|
||||||
|
footer.textContent = "0 rows";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit rendering for performance if massive (simple virtualization cap)
|
||||||
|
const renderLimit = 2000;
|
||||||
|
const dataset = regions.slice(0, renderLimit);
|
||||||
|
|
||||||
|
const html = dataset
|
||||||
|
.map((r) => {
|
||||||
|
const isExecutable = r.perms.includes("x");
|
||||||
|
const isWritable = r.perms.includes("w");
|
||||||
|
const isReadable = r.perms.includes("r");
|
||||||
|
|
||||||
|
// Badges for perms
|
||||||
|
let permBadges = "";
|
||||||
|
if (isReadable)
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 mr-1">R</span>`;
|
||||||
|
else
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-400 dark:text-slate-500 mr-1">-</span>`;
|
||||||
|
|
||||||
|
if (isWritable)
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 dark:bg-amber-900 text-amber-800 dark:text-amber-200 mr-1">W</span>`;
|
||||||
|
else
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-400 dark:text-slate-500 mr-1">-</span>`;
|
||||||
|
|
||||||
|
if (isExecutable)
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200">X</span>`;
|
||||||
|
else
|
||||||
|
permBadges += `<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-slate-100 dark:bg-slate-700 text-slate-400 dark:text-slate-500">-</span>`;
|
||||||
|
|
||||||
|
// Row highlighting
|
||||||
|
const rowClass =
|
||||||
|
r.dirtyCount > 0
|
||||||
|
? "bg-orange-50 dark:bg-orange-900/20 hover:bg-orange-100 dark:hover:bg-orange-900/30"
|
||||||
|
: "hover:bg-slate-50 dark:hover:bg-slate-700";
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr class="${rowClass} transition-colors border-b border-slate-100 dark:border-slate-700 last:border-0">
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap font-mono text-xs text-slate-600 dark:text-slate-400 select-all">${r.startStr}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap font-mono text-xs text-slate-600 dark:text-slate-400 select-all">${r.endStr}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap text-sm text-slate-700 dark:text-slate-300 font-medium" title="${r.size.toString()} bytes">${formatBytes(r.size)}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap text-sm">${permBadges}</td>
|
||||||
|
<td class="px-6 py-3 text-sm text-slate-700 dark:text-slate-300 break-all max-w-xs">${r.name || '<span class="text-slate-300 dark:text-slate-600 italic">unnamed</span>'}</td>
|
||||||
|
<td class="px-6 py-3 whitespace-nowrap text-sm">
|
||||||
|
${
|
||||||
|
r.dirtyCount > 0
|
||||||
|
? `<span class="px-2 py-1 text-xs font-bold leading-none text-orange-800 dark:text-orange-200 bg-orange-200 dark:bg-orange-900/50 rounded-full">${r.dirtyCount}</span>`
|
||||||
|
: '<span class="text-slate-300 dark:text-slate-600 text-xs">-</span>'
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
tbody.innerHTML = html;
|
||||||
|
|
||||||
|
const remaining = regions.length - renderLimit;
|
||||||
|
footer.innerHTML =
|
||||||
|
remaining > 0
|
||||||
|
? `${dataset.length} rows shown (${remaining} hidden for performance)`
|
||||||
|
: `${dataset.length} rows`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers & Utilities ---
|
||||||
|
|
||||||
|
function formatBytes(bigIntBytes) {
|
||||||
|
if (bigIntBytes === 0n) return "0 B";
|
||||||
|
|
||||||
|
// Convert to Number for display formatting (precision loss at PB scale is acceptable for UI)
|
||||||
|
const bytes = Number(bigIntBytes);
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortBy(column) {
|
||||||
|
if (currentSort.column === column) {
|
||||||
|
currentSort.direction = currentSort.direction === "asc" ? "desc" : "asc";
|
||||||
|
} else {
|
||||||
|
currentSort.column = column;
|
||||||
|
currentSort.direction = "asc";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual indicator helper
|
||||||
|
document
|
||||||
|
.querySelectorAll(".sort-icon")
|
||||||
|
.forEach((el) => (el.textContent = "⇅"));
|
||||||
|
document
|
||||||
|
.querySelectorAll(".sort-icon")
|
||||||
|
.forEach((el) => el.classList.remove("text-indigo-600"));
|
||||||
|
|
||||||
|
// Update active header
|
||||||
|
const headers = document.querySelectorAll("th");
|
||||||
|
headers.forEach((th) => {
|
||||||
|
if (th.onclick.toString().includes(column)) {
|
||||||
|
const icon = th.querySelector(".sort-icon");
|
||||||
|
icon.textContent = currentSort.direction === "asc" ? "▲" : "▼";
|
||||||
|
icon.classList.add("text-indigo-600");
|
||||||
|
icon.classList.remove("invisible");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const addrFilter = document
|
||||||
|
.getElementById("addressFilter")
|
||||||
|
.value.toLowerCase();
|
||||||
|
const permsFilter = document.getElementById("permsFilter").value;
|
||||||
|
const dirtyFilter = document.getElementById("dirtyFilter").value;
|
||||||
|
const hideEmpty = document.getElementById("hideEmptyRegions").checked;
|
||||||
|
|
||||||
|
let filtered = allRegions.filter((r) => {
|
||||||
|
// 1. Empty/Permissions check
|
||||||
|
if (hideEmpty && r.perms.startsWith("---")) return false;
|
||||||
|
|
||||||
|
// 2. Address/Name Search
|
||||||
|
if (addrFilter) {
|
||||||
|
const searchStr = (r.startStr + r.endStr + r.name).toLowerCase();
|
||||||
|
if (!searchStr.includes(addrFilter)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Permissions Dropdown
|
||||||
|
if (permsFilter) {
|
||||||
|
if (
|
||||||
|
permsFilter === "rw" &&
|
||||||
|
(!r.perms.includes("r") || !r.perms.includes("w"))
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
else if (
|
||||||
|
permsFilter === "rx" &&
|
||||||
|
(!r.perms.includes("r") || !r.perms.includes("x"))
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
else if (permsFilter.length === 1 && !r.perms.includes(permsFilter))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Dirty Pages
|
||||||
|
if (dirtyFilter === "dirty" && r.dirtyCount === 0) return false;
|
||||||
|
if (dirtyFilter === "clean" && r.dirtyCount > 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply Sort
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
let valA, valB;
|
||||||
|
|
||||||
|
switch (currentSort.column) {
|
||||||
|
case "start":
|
||||||
|
valA = a.startBig;
|
||||||
|
valB = b.startBig;
|
||||||
|
break;
|
||||||
|
case "end":
|
||||||
|
valA = a.endBig;
|
||||||
|
valB = b.endBig;
|
||||||
|
break;
|
||||||
|
case "size":
|
||||||
|
valA = a.size;
|
||||||
|
valB = b.size;
|
||||||
|
break;
|
||||||
|
case "perms":
|
||||||
|
valA = a.perms;
|
||||||
|
valB = b.perms;
|
||||||
|
break; // string comparison
|
||||||
|
case "name":
|
||||||
|
valA = a.name.toLowerCase();
|
||||||
|
valB = b.name.toLowerCase();
|
||||||
|
break;
|
||||||
|
case "dirty":
|
||||||
|
valA = a.dirtyCount;
|
||||||
|
valB = b.dirtyCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valA < valB) return currentSort.direction === "asc" ? -1 : 1;
|
||||||
|
if (valA > valB) return currentSort.direction === "asc" ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
renderTable(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAll() {
|
||||||
|
document.getElementById("input").value = "";
|
||||||
|
document.getElementById("table-body").innerHTML =
|
||||||
|
`<tr><td colspan="6" class="py-12 text-center text-slate-400">Waiting for input...</td></tr>`;
|
||||||
|
document.getElementById("stats").classList.add("hidden");
|
||||||
|
document.getElementById("stats").classList.remove("grid");
|
||||||
|
document.getElementById("filter-section").classList.add("hidden");
|
||||||
|
document.getElementById("filter-section").classList.remove("flex");
|
||||||
|
document.getElementById("footer-count").innerText = "0 rows";
|
||||||
|
|
||||||
|
// Reset filters
|
||||||
|
document.getElementById("addressFilter").value = "";
|
||||||
|
document.getElementById("permsFilter").value = "";
|
||||||
|
document.getElementById("dirtyFilter").value = "";
|
||||||
|
|
||||||
|
allRegions = [];
|
||||||
|
currentSort = { column: null, direction: "asc" };
|
||||||
|
}
|
||||||
41
style.css
Normal file
41
style.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for table container */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: #475569;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user