|
1 | 1 | // viewer.js |
2 | | -// Requires JSZip to handle CRX extraction |
3 | | -// Include JSZip via CDN |
4 | | -const script = document.createElement('script'); |
5 | | -script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.11.0/jszip.min.js"; |
6 | | -document.head.appendChild(script); |
| 2 | +let zip, fileMap = {}; |
| 3 | +const treeRoot = document.getElementById('treeRoot'); |
| 4 | +const viewer = document.getElementById('viewer'); |
| 5 | +const crxInput = document.getElementById('crxInput'); |
| 6 | +const processBtn = document.getElementById('processBtn'); |
| 7 | +const searchBox = document.getElementById('searchBox'); |
| 8 | +const themeToggle = document.getElementById('themeToggle'); |
| 9 | +const exportBtn = document.getElementById('exportBtn'); |
7 | 10 |
|
8 | | -script.onload = () => { |
9 | | - const crxInput = document.getElementById('crxInput'); |
10 | | - const fileList = document.getElementById('fileList'); |
11 | | - const fileContent = document.getElementById('fileContent'); |
| 11 | +let currentTheme = 'dark'; |
12 | 12 |
|
13 | | - crxInput.addEventListener('change', async (e) => { |
14 | | - const file = e.target.files[0]; |
15 | | - if (!file) return; |
16 | | - |
17 | | - fileList.innerHTML = "Loading CRX..."; |
18 | | - |
19 | | - const buffer = await file.arrayBuffer(); |
20 | | - |
21 | | - // CRX Header parsing (CRXv2/v3) |
22 | | - // Skip the header and extract zip portion |
23 | | - let zipStart = 0; |
24 | | - const magic = new Uint8Array(buffer.slice(0, 4)); |
25 | | - if (magic[0] !== 67 || magic[1] !== 82 || magic[2] !== 67 || magic[3] !== 0x21) { |
26 | | - alert("Not a valid CRX file"); |
27 | | - return; |
28 | | - } |
| 13 | +// THEME TOGGLE |
| 14 | +themeToggle.onclick = () => { |
| 15 | + if(currentTheme==='dark'){ document.body.classList.add('light'); currentTheme='light'; } |
| 16 | + else { document.body.classList.remove('light'); currentTheme='dark'; } |
| 17 | +}; |
29 | 18 |
|
30 | | - // CRX v2 header size: 16 + public key length + signature length |
31 | | - const dv = new DataView(buffer); |
32 | | - const version = dv.getUint32(4, true); |
33 | | - if (version === 2) { |
34 | | - const publicKeyLength = dv.getUint32(8, true); |
35 | | - const signatureLength = dv.getUint32(12, true); |
36 | | - zipStart = 16 + publicKeyLength + signatureLength; |
37 | | - } else if (version === 3) { |
38 | | - // CRXv3 uses a header size field at 8 |
39 | | - const headerSize = dv.getUint32(8, true); |
40 | | - zipStart = 12 + headerSize; |
41 | | - } else { |
42 | | - alert("Unsupported CRX version: " + version); |
43 | | - return; |
44 | | - } |
| 19 | +// PROCESS FILE |
| 20 | +processBtn.onclick = async () => { |
| 21 | + if (!crxInput.files[0]) return alert('Please select a CRX file!'); |
| 22 | + const arrayBuffer = await crxInput.files[0].arrayBuffer(); |
| 23 | + zip = await JSZip.loadAsync(arrayBuffer); |
| 24 | + fileMap = {}; // clear previous |
| 25 | + treeRoot.innerHTML = ''; |
| 26 | + buildTree(zip, treeRoot, ''); |
| 27 | +}; |
45 | 28 |
|
46 | | - const zipData = buffer.slice(zipStart); |
| 29 | +// BUILD TREE |
| 30 | +async function buildTree(folder, parent, path) { |
| 31 | + for(const name in folder.files){ |
| 32 | + const f = folder.files[name]; |
| 33 | + const displayName = name.split('/').pop(); |
| 34 | + if(f.dir){ |
| 35 | + const li = document.createElement('li'); |
| 36 | + li.textContent = displayName+'/'; |
| 37 | + const ul = document.createElement('ul'); |
| 38 | + li.appendChild(ul); |
| 39 | + parent.appendChild(li); |
| 40 | + buildTree(f, ul, name); |
| 41 | + } else { |
| 42 | + const li = document.createElement('li'); |
| 43 | + li.textContent = displayName; |
| 44 | + li.onclick = async () => showFile(name, li); |
| 45 | + parent.appendChild(li); |
| 46 | + fileMap[name] = f; |
| 47 | + } |
| 48 | + } |
| 49 | +} |
47 | 50 |
|
48 | | - const zip = await JSZip.loadAsync(zipData); |
49 | | - fileList.innerHTML = ""; |
| 51 | +// SHOW FILE CONTENT |
| 52 | +async function showFile(name, li){ |
| 53 | + const content = await zip.file(name).async('string'); |
| 54 | + viewer.textContent = content; |
| 55 | + Prism.highlightElement(viewer); |
| 56 | + document.querySelectorAll('#fileTree li').forEach(i=>i.classList.remove('selected')); |
| 57 | + li.classList.add('selected'); |
| 58 | +} |
50 | 59 |
|
51 | | - // Populate file list |
52 | | - Object.keys(zip.files).forEach(filename => { |
53 | | - const item = document.createElement('div'); |
54 | | - item.textContent = filename; |
55 | | - item.className = 'file-item'; |
56 | | - item.addEventListener('click', async () => { |
57 | | - const content = await zip.files[filename].async('string'); |
58 | | - fileContent.textContent = content; |
59 | | - }); |
60 | | - fileList.appendChild(item); |
61 | | - }); |
| 60 | +// SEARCH FILES |
| 61 | +searchBox.oninput = ()=>{ |
| 62 | + const q = searchBox.value.toLowerCase(); |
| 63 | + document.querySelectorAll('#fileTree li').forEach(li=>{ |
| 64 | + if(li.textContent.toLowerCase().includes(q)) li.style.display='block'; |
| 65 | + else li.style.display='none'; |
| 66 | + }); |
| 67 | +} |
62 | 68 |
|
63 | | - fileContent.textContent = "Select a file to view its content..."; |
64 | | - }); |
65 | | -}; |
| 69 | +// EXPORT SELECTED FILES |
| 70 | +exportBtn.onclick = async ()=>{ |
| 71 | + if(!zip) return alert('No CRX loaded!'); |
| 72 | + const exportZip = new JSZip(); |
| 73 | + for(const path in fileMap){ |
| 74 | + const content = await zip.file(path).async('arraybuffer'); |
| 75 | + exportZip.file(path, content); |
| 76 | + } |
| 77 | + const blob = await exportZip.generateAsync({type:'blob'}); |
| 78 | + const url = URL.createObjectURL(blob); |
| 79 | + const a = document.createElement('a'); |
| 80 | + a.href = url; |
| 81 | + a.download = 'extracted_extension.zip'; |
| 82 | + a.click(); |
| 83 | + URL.revokeObjectURL(url); |
| 84 | +} |
0 commit comments