diff --git a/index.html b/index.html deleted file mode 100644 index 473872b..0000000 --- a/index.html +++ /dev/null @@ -1,1660 +0,0 @@ - - - - - - - Dateisystem-Offset-Rechner - - - - -
-
- - -
-
- -
-
-

Dateisystem-Offset-Rechner

- -
-
FAT12/16
-
FAT32
-
- - -
-
-
-

Konstanten

-
-
- - -
-
-
- - -
-
-
-
- -
-

Hex-Rechner

-
-
-
- -
-
-
- HEX: - 0x0 -
-
- DEC: - 0 -
-
- BIN: - 0b0 -
-
-
-
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - -
-
-
-
-
- -
-

Eingabeparameter

-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
-
- -
-

Berechnete Werte

- -
-

FAT-Struktur

-
- FAT-Bereich Anfang (Bytes): -
- - - -
-
-
- FAT-Bereich Anfang (Sektor): -
- - - -
-
-
- FAT-Bereich Größe (Bytes): -
- - - -
-
-
- FAT2 Anfang (Bytes): -
- - - -
-
-
- FAT-Bereich Ende (Bytes): -
- - - -
-
-
- -
-

Root-Directory

-
- Root-Directory Anfang (Bytes): -
- - - -
-
-
- Root-Directory Größe (Bytes): -
- - - -
-
-
- -
-

Daten-Bereich

-
- Daten-Bereich Anfang (Bytes): -
- - - -
-
-
- Daten-Bereich Anfang (Sektor): -
- - - -
-
-
- Daten-Bereich Größe (Bytes): -
- - - -
-
-
- Cluster-Größe (Bytes): -
- - - -
-
-
- Anzahl Cluster: -
- - - -
-
-
- -
-

Spezifischer Cluster

-
- Cluster Anfang (Bytes): -
- - - -
-
-
- Cluster Anfang (Sektor): -
- - - -
-
-
- FAT-Eintrag Position (Bytes, nur FAT16): -
- - - -
-
-
-
- -
-

Berechnungsformeln FAT12/16

-
-
FAT-Bereich Anfang = Reservierte Sektoren × Sektorgröße + Basis-Offset
-
FAT-Bereich Größe = Anzahl FATs × FAT-Größe in Sektoren × Sektorgröße
-
FAT2 Anfang = FAT-Bereich Anfang + FAT-Größe in Bytes
-
FAT-Bereich Ende = FAT-Bereich Anfang + FAT-Bereich Größe
-
Root-Directory Größe = Max. Root-Einträge × 0x20
-
Daten-Bereich Anfang = FAT-Bereich Ende + Root-Directory Größe
-
Daten-Bereich Größe = (Partitionsgröße × Sektorgröße) - (Daten-Bereich Anfang - Basis-Offset)
-
Cluster-Größe = Sektorgröße × Cluster-Größe in Sektoren
-
Anzahl Cluster = Daten-Bereich Größe ÷ Cluster-Größe
-
Cluster Anfang = Daten-Bereich Anfang + (Cluster-Nummer - 0x2) × Cluster-Größe
-
FAT-Eintrag Position = FAT-Bereich Anfang + (Cluster-Nummer × Eintraggröße)
-
-
-
- - -
-
-
-

Konstanten

-
-
- - -
-
-
- - -
-
-
-
- -
-

Hex-Rechner

-
-
-
- -
-
-
- HEX: - 0x0 -
-
- DEC: - 0 -
-
- BIN: - 0b0 -
-
-
-
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - -
-
-
-
-
- -
-

Eingabeparameter

-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
-
- -
-

Berechnete Werte

- -
-

FAT-Struktur

-
- FAT-Bereich Anfang (Bytes): -
- - - -
-
-
- FAT-Bereich Anfang (Sektor): -
- - - -
-
-
- FAT-Bereich Größe (Bytes): -
- - - -
-
-
- FAT2 Anfang (Bytes): -
- - - -
-
-
- FAT-Bereich Ende (Bytes): -
- - - -
-
-
- -
-

Daten-Bereich

-
- Daten-Bereich Anfang (Bytes): -
- - - -
-
-
- Daten-Bereich Anfang (Sektor): -
- - - -
-
-
- Daten-Bereich Größe (Bytes): -
- - - -
-
-
- Cluster-Größe (Bytes): -
- - - -
-
-
- Anzahl Cluster: -
- - - -
-
-
- -
-

Spezifischer Cluster

-
- Cluster Anfang (Bytes): -
- - - -
-
-
- Cluster Anfang (Sektor): -
- - - -
-
-
- FAT-Eintrag Position (Bytes): -
- - - -
-
-
- Root-Directory Position (Bytes): -
- - - -
-
-
-
- -
-

Berechnungsformeln FAT32

-
-
FAT-Bereich Anfang = Reservierte Sektoren × Sektorgröße + Basis-Offset
-
FAT-Bereich Größe = Anzahl FATs × FAT-Größe in Sektoren × Sektorgröße
-
FAT2 Anfang = FAT-Bereich Anfang + FAT-Größe in Bytes
-
FAT-Bereich Ende = FAT-Bereich Anfang + FAT-Bereich Größe
-
Daten-Bereich Anfang = FAT-Bereich Ende (kein festes Root Directory)
-
Daten-Bereich Größe = (Partitionsgröße × Sektorgröße) - (Daten-Bereich Anfang - Basis-Offset)
-
Cluster-Größe = Sektorgröße × Cluster-Größe in Sektoren
-
Anzahl Cluster = Daten-Bereich Größe ÷ Cluster-Größe
-
Cluster Anfang = Daten-Bereich Anfang + (Cluster-Nummer - 0x2) × Cluster-Größe
-
FAT-Eintrag Position = FAT-Bereich Anfang + (Cluster-Nummer × 4)
-
Root-Directory Position = Daten-Bereich Anfang + (Root-Directory Cluster - 0x2) × Cluster-Größe
-
-
-
-
-
- - - - - - - \ No newline at end of file diff --git a/webroot/css/components.css b/webroot/css/components.css new file mode 100644 index 0000000..ed81250 --- /dev/null +++ b/webroot/css/components.css @@ -0,0 +1,336 @@ +/* Calculator Styles */ +.hex-calculator { + background-color: #1a1a1a; + min-height: fit-content; +} + +.calculator-container { + display: flex; + flex-direction: column; + gap: 12px; +} + +.calc-display { + background-color: #111111; + border: 1px solid #404040; + border-radius: 6px; + padding: 12px; +} + +.calc-input input { + width: 100%; + background-color: #2a2a2a; + border: 1px solid #555555; + border-radius: 4px; + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 16px; + font-weight: bold; + padding: 8px; + text-align: right; + margin-bottom: 8px; +} + +.calc-conversions { + display: flex; + flex-direction: column; + gap: 4px; +} + +.conversion-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 3px 0; + border-bottom: 1px solid #333333; +} + +.conversion-row:last-child { + border-bottom: none; +} + +.conv-label { + color: #888888; + font-weight: bold; + font-size: 0.8em; + min-width: 30px; +} + +.conv-value { + color: #cccccc; + font-family: 'Consolas', monospace; + font-size: 0.8em; + word-break: break-all; + text-align: right; + flex: 1; + margin-left: 10px; +} + +.calc-buttons { + display: flex; + flex-direction: column; + gap: 4px; +} + +.calc-row { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 4px; +} + +.calc-btn { + background-color: #2a2a2a; + border: 1px solid #4a4a4a; + border-radius: 3px; + color: #ffffff; + cursor: pointer; + font-family: 'Consolas', monospace; + font-size: 0.8em; + font-weight: bold; + min-height: 32px; + padding: 4px; + text-align: center; + transition: all 0.2s; + user-select: none; +} + +.calc-btn:hover { + background-color: #3a3a3a; + border-color: #666666; +} + +.calc-btn:active { + background-color: #444444; + transform: translateY(1px); +} + +.calc-btn.number { + background-color: #333333; +} + +.calc-btn.number:hover { + background-color: #444444; +} + +.calc-btn.operation { + background-color: #2a4a2a; + color: #88cc88; +} + +.calc-btn.operation:hover { + background-color: #3a5a3a; +} + +.calc-btn.clear { + background-color: #4a2a4a; + color: #cc88cc; +} + +.calc-btn.clear:hover { + background-color: #5a3a5a; +} + +.calc-btn.equals { + background-color: #2a2a4a; + color: #8888cc; +} + +.calc-btn.equals:hover { + background-color: #3a3a5a; +} + +.calc-btn.backspace { + background-color: #4a4a2a; + color: #cccc88; +} + +.calc-btn.backspace:hover { + background-color: #5a5a3a; +} + +.calc-btn.wide { + grid-column: span 2; +} + +/* Input Groups */ +.input-group { + display: flex; + flex-direction: column; + position: relative; +} + +.input-group label { + margin-bottom: 6px; + color: #cccccc; + font-weight: normal; + font-size: 0.95em; +} + +.unit-indicator { + color: #88cc88; + font-weight: bold; + font-size: 0.85em; +} + +.input-group input { + padding: 12px; + background-color: #333333; + border: 1px solid #4a4a4a; + border-radius: 4px; + color: #ffffff; + font-family: 'Consolas', monospace; + font-size: 14px; + transition: border-color 0.2s; +} + +.input-group input:focus { + outline: none; + border-color: #666666; + background-color: #383838; +} + +.input-group input.error { + border-color: #ff6666; + background-color: #332222; +} + +.error-message { + color: #ff8888; + font-size: 0.8em; + margin-top: 4px; + font-style: italic; +} + +/* Result Items */ +.result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + border-bottom: 1px solid #3a3a3a; + position: relative; +} + +.result-item:last-child { + border-bottom: none; +} + +.result-item.available { + opacity: 1; +} + +.result-item.unavailable { + opacity: 0.5; +} + +.result-label { + color: #cccccc; + font-weight: normal; + flex: 1; + cursor: help; +} + +.result-value-container { + display: flex; + align-items: center; + gap: 8px; +} + +.result-value { + color: #ffffff; + font-family: 'Consolas', monospace; + text-align: right; + font-weight: bold; + min-width: 120px; + cursor: help; +} + +.copy-btn { + background: none; + border: none; + color: #888888; + cursor: pointer; + padding: 4px; + border-radius: 3px; + transition: all 0.2s; + font-size: 14px; +} + +.copy-btn:hover { + color: #ffffff; + background-color: #404040; +} + +.copy-btn:active { + color: #88cc88; +} + +/* Result Groups */ +.result-group { + margin-bottom: 20px; + border: 1px solid #3a3a3a; + border-radius: 6px; + padding: 15px; + background-color: #2a2a2a; +} + +.result-group h3 { + color: #ffffff; + margin-bottom: 10px; + font-size: 1.1em; + border-bottom: 1px solid #3a3a3a; + padding-bottom: 5px; +} + +/* Formula Styles */ +.formula-box { + background-color: #151515; + border: 1px solid #404040; + border-radius: 4px; + padding: 15px; + margin: 10px 0; + font-family: 'Consolas', monospace; + font-size: 0.9em; +} + +.formula { + margin: 8px 0; + color: #d0d0d0; + word-wrap: break-word; +} + +.calculated { + color: #ffffff; + font-weight: bold; +} + +/* Tooltip */ +.tooltip { + position: absolute; + background-color: #1a1a1a; + color: #ffffff; + padding: 8px 12px; + border-radius: 4px; + font-size: 0.85em; + border: 1px solid #555555; + z-index: 1000; + max-width: 400px; + word-wrap: break-word; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + font-family: 'Consolas', monospace; + line-height: 1.4; +} + +/* Mobile Responsive */ +@media (max-width: 768px) { + .result-item { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } + + .result-value { + text-align: left; + min-width: auto; + } +} \ No newline at end of file diff --git a/webroot/css/main.css b/webroot/css/main.css new file mode 100644 index 0000000..fd2520a --- /dev/null +++ b/webroot/css/main.css @@ -0,0 +1,234 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Consolas', 'Courier New', monospace; + background-color: #1a1a1a; + color: #e0e0e0; + line-height: 1.6; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Header */ +.header { + background-color: #2a2a2a; + border-bottom: 2px solid #3a3a3a; + padding: 15px 0; +} + +.header-content { + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; +} + +.logo { + font-size: 1.5em; + font-weight: bold; + color: #ffffff; +} + +.nav { + display: flex; + gap: 20px; +} + +.nav a { + color: #cccccc; + text-decoration: none; + transition: color 0.2s; +} + +.nav a:hover { + color: #ffffff; +} + +/* Main Content */ +.main-content { + flex: 1; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; +} + +h1 { + text-align: center; + margin-bottom: 30px; + color: #ffffff; + font-size: 2.2em; + font-weight: normal; +} + +/* Tabs */ +.tabs { + display: flex; + background-color: #2a2a2a; + border-radius: 6px 6px 0 0; + border: 1px solid #3a3a3a; + border-bottom: none; + overflow-x: auto; +} + +.tab { + padding: 15px 25px; + cursor: pointer; + background-color: #2a2a2a; + border-right: 1px solid #3a3a3a; + color: #cccccc; + transition: all 0.2s; + white-space: nowrap; + min-width: 120px; + text-align: center; +} + +.tab:last-child { + border-right: none; +} + +.tab:hover { + background-color: #353535; +} + +.tab.active { + background-color: #3a3a3a; + color: #ffffff; + border-bottom: 2px solid #666666; +} + +.tab-content { + display: none; + background-color: #2a2a2a; + border: 1px solid #3a3a3a; + border-radius: 0 0 6px 6px; + padding: 20px; +} + +.tab-content.active { + display: block; +} + +/* Sections */ +.section { + background-color: #252525; + border-radius: 6px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid #3a3a3a; +} + +.section h2 { + color: #cccccc; + margin-bottom: 15px; + border-bottom: 1px solid #3a3a3a; + padding-bottom: 8px; + font-weight: normal; + font-size: 1.3em; +} + +.constants { + background-color: #202020; +} + +.results { + background-color: #202020; +} + +.formulas { + background-color: #1a1a1a; +} + +/* Layout */ +.constants-calculator-container { + display: grid; + grid-template-columns: 1fr 350px; + gap: 20px; + margin-bottom: 20px; +} + +@media (max-width: 1024px) { + .constants-calculator-container { + grid-template-columns: 1fr; + gap: 15px; + } +} + +/* Input Grid */ +.input-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 15px; + margin: 20px 0; +} + +@media (max-width: 768px) { + .input-grid { + grid-template-columns: 1fr; + gap: 12px; + } + + .constants-calculator-container { + grid-template-columns: 1fr; + } +} + +/* Footer */ +.footer { + background-color: #2a2a2a; + border-top: 1px solid #3a3a3a; + padding: 20px 0; + margin-top: 40px; +} + +.footer-content { + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 30px; +} + +.footer-section h3 { + color: #ffffff; + margin-bottom: 10px; + font-size: 1.1em; +} + +.footer-section p, +.footer-section a { + color: #cccccc; + font-size: 0.9em; + text-decoration: none; + margin-bottom: 5px; + display: block; +} + +.footer-section a:hover { + color: #ffffff; +} + +.footer-bottom { + text-align: center; + padding-top: 20px; + margin-top: 20px; + border-top: 1px solid #3a3a3a; + color: #888888; + font-size: 0.85em; +} + +.error { + color: #ff8888; + font-style: italic; +} \ No newline at end of file diff --git a/webroot/index.html b/webroot/index.html new file mode 100644 index 0000000..e586fb7 --- /dev/null +++ b/webroot/index.html @@ -0,0 +1,55 @@ + + + + + + + Dateisystem-Offset-Rechner + + + + + +
+
+ + +
+
+ +
+
+

Dateisystem-Offset-Rechner

+ + +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/webroot/js/calculator.js b/webroot/js/calculator.js new file mode 100644 index 0000000..02bdd5b --- /dev/null +++ b/webroot/js/calculator.js @@ -0,0 +1,243 @@ +// Hex calculator functionality + +export class Calculator { + constructor() { + this.state = { + currentValue: 0, + previousValue: 0, + operation: null, + waitingForOperand: false + }; + } + + updateDisplay() { + const value = Math.max(0, Math.min(Math.floor(this.state.currentValue), Number.MAX_SAFE_INTEGER)); + + const hexStr = '0x' + value.toString(16).toUpperCase(); + const decStr = value.toString(10); + const binStr = '0b' + value.toString(2); + + // Update all calculator displays (for current active tab) + const inputs = document.querySelectorAll('[id*="calcInput"]'); + const hexElements = document.querySelectorAll('[id*="hexValue"]'); + const decElements = document.querySelectorAll('[id*="decValue"]'); + const binElements = document.querySelectorAll('[id*="binValue"]'); + + inputs.forEach(input => input.value = hexStr); + hexElements.forEach(el => el.textContent = hexStr); + decElements.forEach(el => el.textContent = decStr); + binElements.forEach(el => el.textContent = binStr); + } + + input(digit) { + if (this.state.waitingForOperand) { + this.state.currentValue = 0; + this.state.waitingForOperand = false; + } + + const digitValue = parseInt(digit, 16); + if (isNaN(digitValue)) return; + + const newValue = this.state.currentValue * 16 + digitValue; + if (newValue <= Number.MAX_SAFE_INTEGER && newValue >= 0) { + this.state.currentValue = newValue; + } + + this.updateDisplay(); + } + + operation(operation) { + if (this.state.previousValue !== 0 && !this.state.waitingForOperand) { + this.equals(false); + } + + this.state.previousValue = this.state.currentValue; + this.state.operation = operation; + this.state.waitingForOperand = true; + } + + equals(updateDisplay = true) { + if (this.state.operation && this.state.previousValue !== null) { + const prev = Math.floor(this.state.previousValue); + const curr = Math.floor(this.state.currentValue); + + try { + let result = 0; + switch (this.state.operation) { + case '+': + result = prev + curr; + break; + case '-': + result = prev - curr; + break; + case '*': + result = prev * curr; + break; + case '/': + if (curr === 0) { + alert('Division durch Null!'); + return; + } + result = Math.floor(prev / curr); + break; + case 'MOD': + if (curr === 0) { + alert('Modulo durch Null!'); + return; + } + result = prev % curr; + break; + } + + this.state.currentValue = Math.max(0, Math.min(result, Number.MAX_SAFE_INTEGER)); + + } catch (error) { + alert('Berechnungsfehler: ' + error.message); + this.state.currentValue = 0; + } + + this.state.operation = null; + this.state.previousValue = 0; + this.state.waitingForOperand = true; + } + + if (updateDisplay) { + this.updateDisplay(); + } + } + + clear() { + this.state.currentValue = 0; + this.state.previousValue = 0; + this.state.operation = null; + this.state.waitingForOperand = false; + this.updateDisplay(); + } + + backspace() { + if (!this.state.waitingForOperand) { + this.state.currentValue = Math.floor(this.state.currentValue / 16); + this.updateDisplay(); + } + } + + setupEventListeners() { + // Button clicks + document.addEventListener('click', (e) => { + if (e.target.classList.contains('calc-btn')) { + const action = e.target.getAttribute('onclick'); + if (action) { + // Parse the onclick attribute to determine the action + if (action.includes('calcInput')) { + const match = action.match(/calcInput\('([^']+)'\)/); + if (match) this.input(match[1]); + } else if (action.includes('calcOperation')) { + const match = action.match(/calcOperation\('([^']+)'\)/); + if (match) this.operation(match[1]); + } else if (action.includes('calcEquals')) { + this.equals(); + } else if (action.includes('calcClear')) { + this.clear(); + } else if (action.includes('calcBackspace')) { + this.backspace(); + } + } + } + }); + + // Keyboard support + document.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT' && !e.target.readOnly) return; + + const key = e.key.toUpperCase(); + + if (/^[0-9A-F]$/.test(key)) { + e.preventDefault(); + this.input(key); + } else if (key === '+') { + e.preventDefault(); + this.operation('+'); + } else if (key === '-') { + e.preventDefault(); + this.operation('-'); + } else if (key === '*') { + e.preventDefault(); + this.operation('*'); + } else if (key === '/') { + e.preventDefault(); + this.operation('/'); + } else if (key === 'ENTER' || key === '=') { + e.preventDefault(); + this.equals(); + } else if (key === 'ESCAPE' || key === 'C') { + e.preventDefault(); + this.clear(); + } else if (key === 'BACKSPACE') { + e.preventDefault(); + this.backspace(); + } + }); + } + + generateHTML() { + return ` +
+
+
+ +
+
+
+ HEX: + 0x0 +
+
+ DEC: + 0 +
+
+ BIN: + 0b0 +
+
+
+
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + +
+
+
+ `; + } +} \ No newline at end of file diff --git a/webroot/js/filesystems/base.js b/webroot/js/filesystems/base.js new file mode 100644 index 0000000..034931e --- /dev/null +++ b/webroot/js/filesystems/base.js @@ -0,0 +1,170 @@ +// Base filesystem class that defines the common interface for all filesystem implementations + +import { parseHex, validateInput, checkDependencies, updateResultItem } from '../utils.js'; + +export class BaseFilesystem { + constructor(name, variants = []) { + this.name = name; + this.variants = variants; // Array of variant configurations + } + + // Abstract method to be implemented by subclasses + getVariants() { + return this.variants; + } + + // Generate HTML for constants section + generateConstantsHTML(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return ''; + + return ` +
+

Konstanten

+
+ ${variant.constants.map(constant => ` +
+ + +
+
+ `).join('')} +
+
+ `; + } + + // Generate HTML for input parameters section + generateInputsHTML(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return ''; + + return ` +
+

Eingabeparameter

+
+ ${variant.inputs.map(input => ` +
+ + +
+
+ `).join('')} +
+
+ `; + } + + // Generate HTML for results section + generateResultsHTML(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return ''; + + return ` +
+

Berechnete Werte

+ ${variant.resultGroups.map(group => ` +
+

${group.name}

+ ${group.results.map(result => ` +
+ ${result.label}: +
+ - + +
+
+ `).join('')} +
+ `).join('')} +
+ `; + } + + // Generate HTML for formulas section + generateFormulasHTML(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return ''; + + return ` +
+

Berechnungsformeln ${this.name}

+
+ ${variant.formulas.map(formula => ` +
${formula.name} = ${formula.expression}
+ `).join('')} +
+
+ `; + } + + // Generate complete tab content HTML + generateTabContentHTML(variantId, calculatorHTML) { + return ` +
+
+ ${this.generateConstantsHTML(variantId)} +
+

Hex-Rechner

+ ${calculatorHTML} +
+
+ ${this.generateInputsHTML(variantId)} + ${this.generateResultsHTML(variantId)} + ${this.generateFormulasHTML(variantId)} +
+ `; + } + + // Abstract method for calculations - to be implemented by subclasses + calculate(variantId) { + throw new Error('calculate method must be implemented by subclass'); + } + + // Setup input event listeners for this filesystem + setupInputListeners(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return; + + const allInputIds = [ + ...variant.constants.map(c => c.id), + ...variant.inputs.map(i => i.id) + ]; + + allInputIds.forEach(inputId => { + const input = document.getElementById(inputId); + if (input) { + input.addEventListener('input', () => { + validateInput(inputId, input.value); + this.calculate(variantId); + }); + } + }); + } + + // Get all input values for a variant + getInputValues(variantId) { + const variant = this.variants.find(v => v.id === variantId); + if (!variant) return {}; + + const values = {}; + + // Get constants + variant.constants.forEach(constant => { + const element = document.getElementById(constant.id); + if (element) { + values[constant.id] = parseHex(element.value) || 0; + } + }); + + // Get inputs + variant.inputs.forEach(input => { + const element = document.getElementById(input.id); + if (element) { + values[input.id] = parseHex(element.value); + } + }); + + return values; + } +} \ No newline at end of file diff --git a/webroot/js/filesystems/fat.js b/webroot/js/filesystems/fat.js new file mode 100644 index 0000000..1b008df --- /dev/null +++ b/webroot/js/filesystems/fat.js @@ -0,0 +1,398 @@ +// FAT filesystem implementation (FAT12/16 and FAT32) + +import { BaseFilesystem } from './base.js'; +import { checkDependencies, updateResultItem } from '../utils.js'; + +export class FATFilesystem extends BaseFilesystem { + constructor() { + super('FAT', [ + { + id: 'fat1216', + name: 'FAT12/16', + constants: [ + { id: 'baseOffset1216', label: 'Basis-Offset', unit: 'Bytes', default: '0x0' }, + { id: 'sectorSize1216', label: 'Sektorgröße', unit: 'Bytes', default: '0x200' } + ], + inputs: [ + { id: 'reservedSectors1216', label: 'Reservierte Sektoren', unit: 'Anzahl', placeholder: '0x1' }, + { id: 'numFATs1216', label: 'Anzahl FATs', unit: 'Anzahl', placeholder: '0x2' }, + { id: 'fatSizeSectors1216', label: 'FAT-Größe', unit: 'Sektoren', placeholder: '0x4000' }, + { id: 'maxRootEntries1216', label: 'Max. Root-Einträge', unit: 'Anzahl', placeholder: '0x200' }, + { id: 'partitionSizeInSectors1216', label: 'Partitionsgröße', unit: 'Sektoren', placeholder: '0x4015' }, + { id: 'clusterSizeSectors1216', label: 'Clustergröße', unit: 'Sektoren', placeholder: '0x2' }, + { id: 'clusterNumber1216', label: 'Cluster-Nummer', unit: 'Nummer', placeholder: '0x2' } + ], + resultGroups: [ + { + name: 'FAT-Struktur', + results: [ + { id: 'fatStart1216', label: 'FAT-Bereich Anfang (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216'], formula: 'reservedSectors × sectorSize + baseOffset' }, + { id: 'fatStartSector1216', label: 'FAT-Bereich Anfang (Sektor)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216'], formula: 'fatStart ÷ sectorSize' }, + { id: 'fatSize1216', label: 'FAT-Bereich Größe (Bytes)', dependencies: ['numFATs1216', 'fatSizeSectors1216', 'sectorSize1216'], formula: 'numFATs × fatSizeSectors × sectorSize' }, + { id: 'fat2Start1216', label: 'FAT2 Anfang (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'fatSizeSectors1216'], formula: 'fatStart + (fatSizeSectors × sectorSize)' }, + { id: 'fatEnd1216', label: 'FAT-Bereich Ende (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216'], formula: 'fatStart + fatSize' } + ] + }, + { + name: 'Root-Directory', + results: [ + { id: 'rootDirStart1216', label: 'Root-Directory Anfang (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216'], formula: 'fatEnd' }, + { id: 'rootDirSize1216', label: 'Root-Directory Größe (Bytes)', dependencies: ['maxRootEntries1216'], formula: 'maxRootEntries × 0x20' } + ] + }, + { + name: 'Daten-Bereich', + results: [ + { id: 'dataStart1216', label: 'Daten-Bereich Anfang (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216'], formula: 'fatEnd + rootDirSize' }, + { id: 'dataStartSector1216', label: 'Daten-Bereich Anfang (Sektor)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216'], formula: 'dataStart ÷ sectorSize' }, + { id: 'dataSize1216', label: 'Daten-Bereich Größe (Bytes)', dependencies: ['partitionSizeInSectors1216', 'sectorSize1216', 'reservedSectors1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216'], formula: '(partitionSizeInSectors × sectorSize) - (dataStart - baseOffset)' }, + { id: 'clusterSize1216', label: 'Cluster-Größe (Bytes)', dependencies: ['sectorSize1216', 'clusterSizeSectors1216'], formula: 'sectorSize × clusterSizeSectors' }, + { id: 'numClusters1216', label: 'Anzahl Cluster', dependencies: ['partitionSizeInSectors1216', 'sectorSize1216', 'reservedSectors1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216', 'clusterSizeSectors1216'], formula: 'dataSize ÷ clusterSize' } + ] + }, + { + name: 'Spezifischer Cluster', + results: [ + { id: 'clusterStart1216', label: 'Cluster Anfang (Bytes)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216', 'clusterSizeSectors1216', 'clusterNumber1216'], formula: 'dataStart + (clusterNumber - 0x2) × clusterSize' }, + { id: 'clusterStartSector1216', label: 'Cluster Anfang (Sektor)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'numFATs1216', 'fatSizeSectors1216', 'maxRootEntries1216', 'clusterSizeSectors1216', 'clusterNumber1216'], formula: 'clusterStart ÷ sectorSize' }, + { id: 'fatEntryPos1216', label: 'FAT-Eintrag Position (Bytes, nur FAT16)', dependencies: ['reservedSectors1216', 'sectorSize1216', 'baseOffset1216', 'clusterNumber1216'], formula: 'fatStart + (clusterNumber × 2) für FAT16' } + ] + } + ], + formulas: [ + { name: 'FAT-Bereich Anfang', expression: 'Reservierte Sektoren × Sektorgröße + Basis-Offset' }, + { name: 'FAT-Bereich Größe', expression: 'Anzahl FATs × FAT-Größe in Sektoren × Sektorgröße' }, + { name: 'FAT2 Anfang', expression: 'FAT-Bereich Anfang + FAT-Größe in Bytes' }, + { name: 'FAT-Bereich Ende', expression: 'FAT-Bereich Anfang + FAT-Bereich Größe' }, + { name: 'Root-Directory Größe', expression: 'Max. Root-Einträge × 0x20' }, + { name: 'Daten-Bereich Anfang', expression: 'FAT-Bereich Ende + Root-Directory Größe' }, + { name: 'Daten-Bereich Größe', expression: '(Partitionsgröße × Sektorgröße) - (Daten-Bereich Anfang - Basis-Offset)' }, + { name: 'Cluster-Größe', expression: 'Sektorgröße × Cluster-Größe in Sektoren' }, + { name: 'Anzahl Cluster', expression: 'Daten-Bereich Größe ÷ Cluster-Größe' }, + { name: 'Cluster Anfang', expression: 'Daten-Bereich Anfang + (Cluster-Nummer - 0x2) × Cluster-Größe' }, + { name: 'FAT-Eintrag Position', expression: 'FAT-Bereich Anfang + (Cluster-Nummer × Eintraggröße)' } + ] + }, + { + id: 'fat32', + name: 'FAT32', + constants: [ + { id: 'baseOffset32', label: 'Basis-Offset', unit: 'Bytes', default: '0x0' }, + { id: 'sectorSize32', label: 'Sektorgröße', unit: 'Bytes', default: '0x200' } + ], + inputs: [ + { id: 'reservedSectors32', label: 'Reservierte Sektoren', unit: 'Anzahl', placeholder: '0x20' }, + { id: 'numFATs32', label: 'Anzahl FATs', unit: 'Anzahl', placeholder: '0x2' }, + { id: 'fatSizeSectors32', label: 'FAT-Größe', unit: 'Sektoren', placeholder: '0x20000' }, + { id: 'rootDirCluster32', label: 'Root-Directory Cluster', unit: 'Nummer', placeholder: '0x2' }, + { id: 'partitionSizeInSectors32', label: 'Partitionsgröße', unit: 'Sektoren', placeholder: '0x100000' }, + { id: 'clusterSizeSectors32', label: 'Clustergröße', unit: 'Sektoren', placeholder: '0x8' }, + { id: 'clusterNumber32', label: 'Cluster-Nummer', unit: 'Nummer', placeholder: '0x2' } + ], + resultGroups: [ + { + name: 'FAT-Struktur', + results: [ + { id: 'fatStart32', label: 'FAT-Bereich Anfang (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32'], formula: 'reservedSectors × sectorSize + baseOffset' }, + { id: 'fatStartSector32', label: 'FAT-Bereich Anfang (Sektor)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32'], formula: 'fatStart ÷ sectorSize' }, + { id: 'fatSize32', label: 'FAT-Bereich Größe (Bytes)', dependencies: ['numFATs32', 'fatSizeSectors32', 'sectorSize32'], formula: 'numFATs × fatSizeSectors × sectorSize' }, + { id: 'fat2Start32', label: 'FAT2 Anfang (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'fatSizeSectors32'], formula: 'fatStart + (fatSizeSectors × sectorSize)' }, + { id: 'fatEnd32', label: 'FAT-Bereich Ende (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32'], formula: 'fatStart + fatSize' } + ] + }, + { + name: 'Daten-Bereich', + results: [ + { id: 'dataStart32', label: 'Daten-Bereich Anfang (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32'], formula: 'fatEnd' }, + { id: 'dataStartSector32', label: 'Daten-Bereich Anfang (Sektor)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32'], formula: 'dataStart ÷ sectorSize' }, + { id: 'dataSize32', label: 'Daten-Bereich Größe (Bytes)', dependencies: ['partitionSizeInSectors32', 'sectorSize32', 'reservedSectors32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32'], formula: '(partitionSizeInSectors × sectorSize) - (dataStart - baseOffset)' }, + { id: 'clusterSize32', label: 'Cluster-Größe (Bytes)', dependencies: ['sectorSize32', 'clusterSizeSectors32'], formula: 'sectorSize × clusterSizeSectors' }, + { id: 'numClusters32', label: 'Anzahl Cluster', dependencies: ['partitionSizeInSectors32', 'sectorSize32', 'reservedSectors32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32', 'clusterSizeSectors32'], formula: 'dataSize ÷ clusterSize' } + ] + }, + { + name: 'Spezifischer Cluster', + results: [ + { id: 'clusterStart32', label: 'Cluster Anfang (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32', 'clusterSizeSectors32', 'clusterNumber32'], formula: 'dataStart + (clusterNumber - 0x2) × clusterSize' }, + { id: 'clusterStartSector32', label: 'Cluster Anfang (Sektor)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32', 'clusterSizeSectors32', 'clusterNumber32'], formula: 'clusterStart ÷ sectorSize' }, + { id: 'fatEntryPos32', label: 'FAT-Eintrag Position (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'clusterNumber32'], formula: 'fatStart + (clusterNumber × 4)' }, + { id: 'rootDirPos32', label: 'Root-Directory Position (Bytes)', dependencies: ['reservedSectors32', 'sectorSize32', 'baseOffset32', 'numFATs32', 'fatSizeSectors32', 'clusterSizeSectors32', 'rootDirCluster32'], formula: 'dataStart + (rootDirCluster - 0x2) × clusterSize' } + ] + } + ], + formulas: [ + { name: 'FAT-Bereich Anfang', expression: 'Reservierte Sektoren × Sektorgröße + Basis-Offset' }, + { name: 'FAT-Bereich Größe', expression: 'Anzahl FATs × FAT-Größe in Sektoren × Sektorgröße' }, + { name: 'FAT2 Anfang', expression: 'FAT-Bereich Anfang + FAT-Größe in Bytes' }, + { name: 'FAT-Bereich Ende', expression: 'FAT-Bereich Anfang + FAT-Bereich Größe' }, + { name: 'Daten-Bereich Anfang', expression: 'FAT-Bereich Ende (kein festes Root Directory)' }, + { name: 'Daten-Bereich Größe', expression: '(Partitionsgröße × Sektorgröße) - (Daten-Bereich Anfang - Basis-Offset)' }, + { name: 'Cluster-Größe', expression: 'Sektorgröße × Cluster-Größe in Sektoren' }, + { name: 'Anzahl Cluster', expression: 'Daten-Bereich Größe ÷ Cluster-Größe' }, + { name: 'Cluster Anfang', expression: 'Daten-Bereich Anfang + (Cluster-Nummer - 0x2) × Cluster-Größe' }, + { name: 'FAT-Eintrag Position', expression: 'FAT-Bereich Anfang + (Cluster-Nummer × 4)' }, + { name: 'Root-Directory Position', expression: 'Daten-Bereich Anfang + (Root-Directory Cluster - 0x2) × Cluster-Größe' } + ] + } + ]); + } + + calculate(variantId) { + const values = this.getInputValues(variantId); + const results = {}; + + if (variantId === 'fat1216') { + this.calculateFAT1216(values, results); + } else if (variantId === 'fat32') { + this.calculateFAT32(values, results); + } + } + + calculateFAT1216(values, results) { + const suffix = '1216'; + + // FAT Start + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize${suffix}`, `baseOffset${suffix}`])) { + results.fatStart = values.reservedSectors1216 * values.sectorSize1216 + values.baseOffset1216; + results.fatStartSector = Math.floor(results.fatStart / values.sectorSize1216); + updateResultItem(`fatStart${suffix}`, results.fatStart, true, + `0x${values.reservedSectors1216.toString(16)} × 0x${values.sectorSize1216.toString(16)} + 0x${values.baseOffset1216.toString(16)} = 0x${results.fatStart.toString(16)}`); + updateResultItem(`fatStartSector${suffix}`, results.fatStartSector, true, + `0x${results.fatStart.toString(16)} ÷ 0x${values.sectorSize1216.toString(16)} = 0x${results.fatStartSector.toString(16)}`); + } else { + updateResultItem(`fatStart${suffix}`, 0, false); + updateResultItem(`fatStartSector${suffix}`, 0, false); + } + + // FAT Size + if (checkDependencies([`numFATs${suffix}`, `fatSizeSectors${suffix}`, `sectorSize${suffix}`])) { + results.fatSize = values.numFATs1216 * values.fatSizeSectors1216 * values.sectorSize1216; + updateResultItem(`fatSize${suffix}`, results.fatSize, true, + `0x${values.numFATs1216.toString(16)} × 0x${values.fatSizeSectors1216.toString(16)} × 0x${values.sectorSize1216.toString(16)} = 0x${results.fatSize.toString(16)}`); + } else { + updateResultItem(`fatSize${suffix}`, 0, false); + } + + // FAT2 Start + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize${suffix}`, `baseOffset${suffix}`, `fatSizeSectors${suffix}`]) && results.fatStart !== undefined) { + results.fat2Start = results.fatStart + (values.fatSizeSectors1216 * values.sectorSize1216); + updateResultItem(`fat2Start${suffix}`, results.fat2Start, true, + `0x${results.fatStart.toString(16)} + 0x${(values.fatSizeSectors1216 * values.sectorSize1216).toString(16)} = 0x${results.fat2Start.toString(16)}`); + } else { + updateResultItem(`fat2Start${suffix}`, 0, false); + } + + // FAT End + if (results.fatStart !== undefined && results.fatSize !== undefined) { + results.fatEnd = results.fatStart + results.fatSize; + updateResultItem(`fatEnd${suffix}`, results.fatEnd, true, + `0x${results.fatStart.toString(16)} + 0x${results.fatSize.toString(16)} = 0x${results.fatEnd.toString(16)}`); + } else { + updateResultItem(`fatEnd${suffix}`, 0, false); + } + + // Root Directory Size + if (checkDependencies(['maxRootEntries1216'])) { + results.rootDirSize = values.maxRootEntries1216 * 0x20; + updateResultItem('rootDirSize1216', results.rootDirSize, true, + `0x${values.maxRootEntries1216.toString(16)} × 0x20 = 0x${results.rootDirSize.toString(16)}`); + } else { + updateResultItem('rootDirSize1216', 0, false); + } + + // Root Directory Start + if (results.fatEnd !== undefined) { + results.rootDirStart = results.fatEnd; + updateResultItem('rootDirStart1216', results.rootDirStart, true, + `0x${results.fatEnd.toString(16)}`); + } else { + updateResultItem('rootDirStart1216', 0, false); + } + + // Data Start + if (results.fatEnd !== undefined && results.rootDirSize !== undefined) { + results.dataStart = results.fatEnd + results.rootDirSize; + results.dataStartSector = Math.floor(results.dataStart / values.sectorSize1216); + updateResultItem('dataStart1216', results.dataStart, true, + `0x${results.fatEnd.toString(16)} + 0x${results.rootDirSize.toString(16)} = 0x${results.dataStart.toString(16)}`); + updateResultItem('dataStartSector1216', results.dataStartSector, true, + `0x${results.dataStart.toString(16)} ÷ 0x${values.sectorSize1216.toString(16)} = 0x${results.dataStartSector.toString(16)}`); + } else { + updateResultItem('dataStart1216', 0, false); + updateResultItem('dataStartSector1216', 0, false); + } + + // Data Size + const requiredForDataSize = [`partitionSizeInSectors${suffix}`, `sectorSize1216`, `reservedSectors${suffix}`, `baseOffset1216`, `numFATs${suffix}`, `fatSizeSectors${suffix}`, 'maxRootEntries1216']; + if (checkDependencies(requiredForDataSize) && results.dataStart !== undefined) { + const partitionSizeBytes = values.partitionSizeInSectors1216 * values.sectorSize1216; + results.dataSize = partitionSizeBytes - (results.dataStart - values.baseOffset1216); + updateResultItem(`dataSize${suffix}`, results.dataSize, true, + `(0x${values.partitionSizeInSectors1216.toString(16)} × 0x${values.sectorSize1216.toString(16)}) - (0x${results.dataStart.toString(16)} - 0x${values.baseOffset1216.toString(16)}) = 0x${results.dataSize.toString(16)}`); + } else { + updateResultItem(`dataSize${suffix}`, 0, false); + } + + // Cluster Size + if (checkDependencies([`sectorSize1216`, `clusterSizeSectors${suffix}`])) { + results.clusterSize = values.sectorSize1216 * values.clusterSizeSectors1216; + updateResultItem(`clusterSize${suffix}`, results.clusterSize, true, + `0x${values.sectorSize1216.toString(16)} × 0x${values.clusterSizeSectors1216.toString(16)} = 0x${results.clusterSize.toString(16)}`); + } else { + updateResultItem(`clusterSize${suffix}`, 0, false); + } + + // Number of Clusters + if (results.dataSize !== undefined && results.clusterSize !== undefined && results.clusterSize > 0) { + results.numClusters = Math.floor(results.dataSize / results.clusterSize); + updateResultItem(`numClusters${suffix}`, results.numClusters, true, + `0x${results.dataSize.toString(16)} ÷ 0x${results.clusterSize.toString(16)} = 0x${results.numClusters.toString(16)}`); + } else { + updateResultItem(`numClusters${suffix}`, 0, false); + } + + // Specific Cluster Start + const requiredForCluster = [`reservedSectors${suffix}`, `sectorSize1216`, `baseOffset1216`, `numFATs${suffix}`, `fatSizeSectors${suffix}`, 'maxRootEntries1216', `clusterSizeSectors${suffix}`, `clusterNumber${suffix}`]; + if (checkDependencies(requiredForCluster) && results.dataStart !== undefined && results.clusterSize !== undefined) { + results.clusterStart = results.dataStart + (values.clusterNumber1216 - 0x2) * results.clusterSize; + results.clusterStartSector = Math.floor(results.clusterStart / values.sectorSize1216); + updateResultItem(`clusterStart${suffix}`, results.clusterStart, true, + `0x${results.dataStart.toString(16)} + (0x${values.clusterNumber1216.toString(16)} - 0x2) × 0x${results.clusterSize.toString(16)} = 0x${results.clusterStart.toString(16)}`); + updateResultItem(`clusterStartSector${suffix}`, results.clusterStartSector, true, + `0x${results.clusterStart.toString(16)} ÷ 0x${values.sectorSize1216.toString(16)} = 0x${results.clusterStartSector.toString(16)}`); + } else { + updateResultItem(`clusterStart${suffix}`, 0, false); + updateResultItem(`clusterStartSector${suffix}`, 0, false); + } + + // FAT Entry Position + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize1216`, `baseOffset1216`, `clusterNumber${suffix}`]) && results.fatStart !== undefined) { + const entrySize = 2; // FAT16 uses 2 bytes per entry + results.fatEntryPos = results.fatStart + (values.clusterNumber1216 * entrySize); + updateResultItem(`fatEntryPos${suffix}`, results.fatEntryPos, true, + `0x${results.fatStart.toString(16)} + (0x${values.clusterNumber1216.toString(16)} × ${entrySize}) = 0x${results.fatEntryPos.toString(16)}`); + } else { + updateResultItem(`fatEntryPos${suffix}`, 0, false); + } + } + + calculateFAT32(values, results) { + const suffix = '32'; + + // FAT Start + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize${suffix}`, `baseOffset${suffix}`])) { + results.fatStart = values.reservedSectors32 * values.sectorSize32 + values.baseOffset32; + results.fatStartSector = Math.floor(results.fatStart / values.sectorSize32); + updateResultItem(`fatStart${suffix}`, results.fatStart, true, + `0x${values.reservedSectors32.toString(16)} × 0x${values.sectorSize32.toString(16)} + 0x${values.baseOffset32.toString(16)} = 0x${results.fatStart.toString(16)}`); + updateResultItem(`fatStartSector${suffix}`, results.fatStartSector, true, + `0x${results.fatStart.toString(16)} ÷ 0x${values.sectorSize32.toString(16)} = 0x${results.fatStartSector.toString(16)}`); + } else { + updateResultItem(`fatStart${suffix}`, 0, false); + updateResultItem(`fatStartSector${suffix}`, 0, false); + } + + // FAT Size + if (checkDependencies([`numFATs${suffix}`, `fatSizeSectors${suffix}`, `sectorSize${suffix}`])) { + results.fatSize = values.numFATs32 * values.fatSizeSectors32 * values.sectorSize32; + updateResultItem(`fatSize${suffix}`, results.fatSize, true, + `0x${values.numFATs32.toString(16)} × 0x${values.fatSizeSectors32.toString(16)} × 0x${values.sectorSize32.toString(16)} = 0x${results.fatSize.toString(16)}`); + } else { + updateResultItem(`fatSize${suffix}`, 0, false); + } + + // FAT2 Start + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize${suffix}`, `baseOffset${suffix}`, `fatSizeSectors${suffix}`]) && results.fatStart !== undefined) { + results.fat2Start = results.fatStart + (values.fatSizeSectors32 * values.sectorSize32); + updateResultItem(`fat2Start${suffix}`, results.fat2Start, true, + `0x${results.fatStart.toString(16)} + 0x${(values.fatSizeSectors32 * values.sectorSize32).toString(16)} = 0x${results.fat2Start.toString(16)}`); + } else { + updateResultItem(`fat2Start${suffix}`, 0, false); + } + + // FAT End + if (results.fatStart !== undefined && results.fatSize !== undefined) { + results.fatEnd = results.fatStart + results.fatSize; + updateResultItem(`fatEnd${suffix}`, results.fatEnd, true, + `0x${results.fatStart.toString(16)} + 0x${results.fatSize.toString(16)} = 0x${results.fatEnd.toString(16)}`); + } else { + updateResultItem(`fatEnd${suffix}`, 0, false); + } + + // Data Start (FAT32 has no fixed root directory) + if (results.fatEnd !== undefined) { + results.dataStart = results.fatEnd; + results.dataStartSector = Math.floor(results.dataStart / values.sectorSize32); + updateResultItem('dataStart32', results.dataStart, true, + `0x${results.fatEnd.toString(16)}`); + updateResultItem('dataStartSector32', results.dataStartSector, true, + `0x${results.dataStart.toString(16)} ÷ 0x${values.sectorSize32.toString(16)} = 0x${results.dataStartSector.toString(16)}`); + } else { + updateResultItem('dataStart32', 0, false); + updateResultItem('dataStartSector32', 0, false); + } + + // Data Size + const requiredForDataSize = [`partitionSizeInSectors${suffix}`, `sectorSize32`, `reservedSectors${suffix}`, `baseOffset32`, `numFATs${suffix}`, `fatSizeSectors${suffix}`]; + if (checkDependencies(requiredForDataSize) && results.dataStart !== undefined) { + const partitionSizeBytes = values.partitionSizeInSectors32 * values.sectorSize32; + results.dataSize = partitionSizeBytes - (results.dataStart - values.baseOffset32); + updateResultItem(`dataSize${suffix}`, results.dataSize, true, + `(0x${values.partitionSizeInSectors32.toString(16)} × 0x${values.sectorSize32.toString(16)}) - (0x${results.dataStart.toString(16)} - 0x${values.baseOffset32.toString(16)}) = 0x${results.dataSize.toString(16)}`); + } else { + updateResultItem(`dataSize${suffix}`, 0, false); + } + + // Cluster Size + if (checkDependencies([`sectorSize32`, `clusterSizeSectors${suffix}`])) { + results.clusterSize = values.sectorSize32 * values.clusterSizeSectors32; + updateResultItem(`clusterSize${suffix}`, results.clusterSize, true, + `0x${values.sectorSize32.toString(16)} × 0x${values.clusterSizeSectors32.toString(16)} = 0x${results.clusterSize.toString(16)}`); + } else { + updateResultItem(`clusterSize${suffix}`, 0, false); + } + + // Number of Clusters + if (results.dataSize !== undefined && results.clusterSize !== undefined && results.clusterSize > 0) { + results.numClusters = Math.floor(results.dataSize / results.clusterSize); + updateResultItem(`numClusters${suffix}`, results.numClusters, true, + `0x${results.dataSize.toString(16)} ÷ 0x${results.clusterSize.toString(16)} = 0x${results.numClusters.toString(16)}`); + } else { + updateResultItem(`numClusters${suffix}`, 0, false); + } + + // Specific Cluster Start + const requiredForCluster = [`reservedSectors${suffix}`, `sectorSize32`, `baseOffset32`, `numFATs${suffix}`, `fatSizeSectors${suffix}`, `clusterSizeSectors${suffix}`, `clusterNumber${suffix}`]; + if (checkDependencies(requiredForCluster) && results.dataStart !== undefined && results.clusterSize !== undefined) { + results.clusterStart = results.dataStart + (values.clusterNumber32 - 0x2) * results.clusterSize; + results.clusterStartSector = Math.floor(results.clusterStart / values.sectorSize32); + updateResultItem(`clusterStart${suffix}`, results.clusterStart, true, + `0x${results.dataStart.toString(16)} + (0x${values.clusterNumber32.toString(16)} - 0x2) × 0x${results.clusterSize.toString(16)} = 0x${results.clusterStart.toString(16)}`); + updateResultItem(`clusterStartSector${suffix}`, results.clusterStartSector, true, + `0x${results.clusterStart.toString(16)} ÷ 0x${values.sectorSize32.toString(16)} = 0x${results.clusterStartSector.toString(16)}`); + } else { + updateResultItem(`clusterStart${suffix}`, 0, false); + updateResultItem(`clusterStartSector${suffix}`, 0, false); + } + + // FAT Entry Position + if (checkDependencies([`reservedSectors${suffix}`, `sectorSize32`, `baseOffset32`, `clusterNumber${suffix}`]) && results.fatStart !== undefined) { + const entrySize = 4; // FAT32 uses 4 bytes per entry + results.fatEntryPos = results.fatStart + (values.clusterNumber32 * entrySize); + updateResultItem(`fatEntryPos${suffix}`, results.fatEntryPos, true, + `0x${results.fatStart.toString(16)} + (0x${values.clusterNumber32.toString(16)} × ${entrySize}) = 0x${results.fatEntryPos.toString(16)}`); + } else { + updateResultItem(`fatEntryPos${suffix}`, 0, false); + } + + // Root Directory Position (FAT32 specific) + const requiredForRootPos = [`reservedSectors${suffix}`, `sectorSize32`, `baseOffset32`, `numFATs${suffix}`, `fatSizeSectors${suffix}`, `clusterSizeSectors${suffix}`, `rootDirCluster32`]; + if (checkDependencies(requiredForRootPos) && results.dataStart !== undefined && results.clusterSize !== undefined) { + results.rootDirPos = results.dataStart + (values.rootDirCluster32 - 0x2) * results.clusterSize; + updateResultItem('rootDirPos32', results.rootDirPos, true, + `0x${results.dataStart.toString(16)} + (0x${values.rootDirCluster32.toString(16)} - 0x2) × 0x${results.clusterSize.toString(16)} = 0x${results.rootDirPos.toString(16)}`); + } else { + updateResultItem('rootDirPos32', 0, false); + } + } +} \ No newline at end of file diff --git a/webroot/js/main.js b/webroot/js/main.js new file mode 100644 index 0000000..13a5ff7 --- /dev/null +++ b/webroot/js/main.js @@ -0,0 +1,139 @@ +// Main application file that coordinates all modules + +import { setupTooltips, setupCopyButtons } from './utils.js'; +import { Calculator } from './calculator.js'; +import { FATFilesystem } from './filesystems/fat.js'; + +class FilesystemCalculator { + constructor() { + this.filesystems = [ + new FATFilesystem() + ]; + this.calculator = new Calculator(); + this.activeTab = null; + } + + init() { + this.renderTabs(); + this.renderContent(); + this.setupEventListeners(); + this.setupCalculator(); + this.setupUtilities(); + + // Activate first tab + if (this.filesystems[0] && this.filesystems[0].variants[0]) { + this.switchTab(this.filesystems[0].variants[0].id); + } + } + + renderTabs() { + const tabsContainer = document.getElementById('filesystem-tabs'); + const tabs = []; + + this.filesystems.forEach(filesystem => { + filesystem.variants.forEach(variant => { + tabs.push(` +
+ ${variant.name} +
+ `); + }); + }); + + tabsContainer.innerHTML = tabs.join(''); + } + + renderContent() { + const contentContainer = document.getElementById('filesystem-content'); + const calculatorHTML = this.calculator.generateHTML(); + const contents = []; + + this.filesystems.forEach(filesystem => { + filesystem.variants.forEach(variant => { + contents.push(filesystem.generateTabContentHTML(variant.id, calculatorHTML)); + }); + }); + + contentContainer.innerHTML = contents.join(''); + } + + setupEventListeners() { + // Tab switching + document.addEventListener('click', (e) => { + if (e.target.classList.contains('tab')) { + const tabId = e.target.getAttribute('data-tab'); + this.switchTab(tabId); + } + }); + + // Input listeners for all filesystems + this.filesystems.forEach(filesystem => { + filesystem.variants.forEach(variant => { + filesystem.setupInputListeners(variant.id); + }); + }); + } + + setupCalculator() { + this.calculator.setupEventListeners(); + + // Make calculator functions globally available for onclick handlers + window.calcInput = (digit) => this.calculator.input(digit); + window.calcOperation = (operation) => this.calculator.operation(operation); + window.calcEquals = () => this.calculator.equals(); + window.calcClear = () => this.calculator.clear(); + window.calcBackspace = () => this.calculator.backspace(); + } + + setupUtilities() { + setupTooltips(); + setupCopyButtons(); + + // Make copyToClipboard globally available for onclick handlers + window.copyToClipboard = (elementId) => { + import('./utils.js').then(module => { + module.copyToClipboard(elementId); + }); + }; + } + + switchTab(tabId) { + // Update tab appearance + document.querySelectorAll('.tab').forEach(tab => { + tab.classList.remove('active'); + }); + document.querySelector(`[data-tab="${tabId}"]`).classList.add('active'); + + // Update content visibility + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + document.getElementById(tabId).classList.add('active'); + + this.activeTab = tabId; + + // Update calculator display + this.calculator.updateDisplay(); + + // Trigger calculation for the new tab + const filesystem = this.findFilesystemForTab(tabId); + if (filesystem) { + filesystem.calculate(tabId); + } + } + + findFilesystemForTab(tabId) { + for (const filesystem of this.filesystems) { + if (filesystem.variants.some(variant => variant.id === tabId)) { + return filesystem; + } + } + return null; + } +} + +// Initialize the application when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + const app = new FilesystemCalculator(); + app.init(); +}); \ No newline at end of file diff --git a/webroot/js/utils.js b/webroot/js/utils.js new file mode 100644 index 0000000..c7e9ca6 --- /dev/null +++ b/webroot/js/utils.js @@ -0,0 +1,166 @@ +// Shared utility functions for the filesystem calculator + +export function parseHex(value) { + if (!value) return 0; + let cleanValue = value.toString().trim(); + if (cleanValue.startsWith('0x') || cleanValue.startsWith('0X')) { + cleanValue = cleanValue.substring(2); + } + const parsed = parseInt(cleanValue, 16); + return isNaN(parsed) ? null : Math.max(0, parsed); +} + +export function formatHex(value) { + if (isNaN(value) || value < 0) { + return 'Fehler'; + } + return '0x' + Math.floor(value).toString(16).toUpperCase(); +} + +export function validateInput(fieldId, value) { + const parsedValue = parseHex(value); + const errorElement = document.getElementById(fieldId + '-error'); + const inputElement = document.getElementById(fieldId); + + if (parsedValue === null && value.trim() !== '') { + errorElement.textContent = 'Ungültiger Hex-Wert'; + inputElement.classList.add('error'); + return false; + } else { + errorElement.textContent = ''; + inputElement.classList.remove('error'); + return true; + } +} + +export function checkDependencies(deps) { + return deps.every(dep => { + const element = document.getElementById(dep); + return element && element.value.trim() !== '' && validateInput(dep, element.value); + }); +} + +export function updateResultItem(elementId, value, available, calculation = '') { + const element = document.getElementById(elementId); + if (!element) return; + + const resultItem = element.closest('.result-item'); + + if (available) { + element.innerHTML = formatHex(value); + resultItem.classList.remove('unavailable'); + resultItem.classList.add('available'); + if (calculation) { + element.setAttribute('data-result-formula', calculation); + } + } else { + element.innerHTML = '-'; + resultItem.classList.remove('available'); + resultItem.classList.add('unavailable'); + element.removeAttribute('data-result-formula'); + } +} + +export function copyToClipboard(elementId) { + const element = document.getElementById(elementId); + if (!element) return; + + const text = element.textContent.trim(); + + if (text === '-' || text.includes('Fehler')) { + return; + } + + navigator.clipboard.writeText(text).then(() => { + const btn = element.parentElement.querySelector('.copy-btn'); + if (btn) { + const originalText = btn.textContent; + btn.textContent = '✓'; + btn.style.color = '#88cc88'; + setTimeout(() => { + btn.textContent = originalText; + btn.style.color = ''; + }, 1000); + } + }).catch(err => { + console.error('Fehler beim Kopieren:', err); + }); +} + +// Tooltip functionality +let tooltip = null; + +export function createTooltip() { + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.style.display = 'none'; + document.body.appendChild(tooltip); + } +} + +export function showTooltip(element, text) { + createTooltip(); + tooltip.innerHTML = text; + tooltip.style.display = 'block'; + + const rect = element.getBoundingClientRect(); + const tooltipRect = tooltip.getBoundingClientRect(); + + let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); + let top = rect.top - tooltipRect.height - 8; + + if (left < 10) left = 10; + if (left + tooltipRect.width > window.innerWidth - 10) { + left = window.innerWidth - tooltipRect.width - 10; + } + if (top < 10) { + top = rect.bottom + 8; + } + + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; +} + +export function hideTooltip() { + if (tooltip) { + tooltip.style.display = 'none'; + } +} + +// Setup tooltip event listeners +export function setupTooltips() { + document.addEventListener('mouseover', function (e) { + if (e.target.classList.contains('result-label')) { + const formula = e.target.getAttribute('data-formula'); + if (formula) { + showTooltip(e.target, `Formel: ${formula}`); + } + } else if (e.target.classList.contains('result-value')) { + const formula = e.target.getAttribute('data-result-formula'); + if (formula && formula !== '') { + showTooltip(e.target, `Berechnung: ${formula}`); + } + } + }); + + document.addEventListener('mouseout', function (e) { + if (e.target.classList.contains('result-label') || e.target.classList.contains('result-value')) { + hideTooltip(); + } + }); + + document.addEventListener('scroll', hideTooltip); +} + +// Setup copy button functionality +export function setupCopyButtons() { + document.addEventListener('click', function (e) { + if (e.target.classList.contains('copy-btn')) { + const resultValue = e.target.parentElement.querySelector('.result-value'); + if (resultValue) { + copyToClipboard(resultValue.id); + } + } + }); +} \ No newline at end of file