// HFS+ (Hierarchical File System Plus) filesystem implementation // ⚠️ Important: All multi-byte values in HFS+ are stored in Big-Endian byte order! import { BaseFilesystem } from './base.js'; import { checkDependencies, updateResultItem } from '../utils.js'; export class HFSPlusFilesystem extends BaseFilesystem { constructor() { super('HFS+', [ { id: 'hfsplus', name: 'HFS+', constants: [ { id: 'partitionStartHFS', label: 'Partitionsstart (Basis-Offset)', unit: 'Bytes', default: '0x5000' }, { id: 'volumeHeaderOffsetHFS', label: 'Volume Header Offset (relativ zur Partition)', unit: 'Bytes', default: '0x400' } ], inputs: [ { id: 'blockSizeHFS', label: 'Block-Größe (Volume Header Offset 0x28)', unit: 'Bytes', placeholder: '0x1000' }, { id: 'totalBlocksHFS', label: 'Partitionsgröße (Volume Header Offset 0x2C)', unit: 'Blöcke', placeholder: '0x10000' }, { id: 'catalogStartBlockHFS', label: 'Fork Catalog File > Catalog File Startblock (Volume Header Offset 0x110 + 0x10)', unit: 'Block-Nummer', placeholder: '0x354' }, { id: 'catalogSizeBlocksHFS', label: 'Fork Catalog File > Catalog File Größe (Volume Header Offset 0x110 + 0x0C)', unit: 'Blöcke', placeholder: '0x3D' }, { id: 'catalogRootNodeHFS', label: 'Knotennummer Wurzelknoten (Catalog File Offset 0x10 = Header Node 0x0E + 0x02)', unit: 'Knotennummer', placeholder: '0x3' }, { id: 'catalogNodeSizeHFS', label: 'Knoten-Größe (Catalog File Offset 0x20 = Header Node 0x0E + 0x12)', unit: 'Bytes', placeholder: '0x1000' }, { id: 'targetNodeNumberHFS', label: 'B-Baum Knotennummer (für Offset-Berechnung)', unit: 'Nummer', placeholder: '0x2' }, { id: 'targetBlockNumberHFS', label: 'Ziel-Block-Nummer', unit: 'Nummer', placeholder: '0x100' }, { id: 'dataForkSizeBytesHFS', label: 'Data Fork Größe (aus Catalog Data Offset 0x58 + 0x00)', unit: 'Bytes', placeholder: '0x7C26E' }, { id: 'dataForkStartBlockHFS', label: 'Data Fork Startblock (aus Catalog Data Offset 0x58 + 0x10)', unit: 'Block-Nummer', placeholder: '0xFC' }, { id: 'resourceForkSizeBytesHFS', label: 'Resource Fork Größe (aus Catalog Data Offset 0xA8 + 0x00)', unit: 'Bytes', placeholder: '0x0' } ], resultGroups: [ { name: 'Volume Header', results: [ { id: 'volumeHeaderPosHFS', label: 'Volume Header Position', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Partitionsstart + 0x400' }, { id: 'partitionSizeBytesHFS', label: 'Partitionsgröße (Bytes)', dependencies: ['totalBlocksHFS', 'blockSizeHFS'], formula: 'Gesamtblöcke × Block-Größe' }, { id: 'backupVolumeHeaderPosHFS', label: 'Backup Volume Header Position', dependencies: ['partitionStartHFS', 'totalBlocksHFS', 'blockSizeHFS'], formula: 'Partitionsstart + Partitionsgröße - 0x200' } ] }, { name: 'Special Files (aus Volume Header Forks)', results: [ { id: 'allocationFileForkOffsetHFS', label: 'Allocation File Fork', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Volume Header Position + 0x70' }, { id: 'extentsFileForkOffsetHFS', label: 'Extents File Fork', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Volume Header Position + 0xC0' }, { id: 'catalogFileForkOffsetHFS', label: 'Catalog File Fork', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Volume Header Position + 0x110' }, { id: 'attributesFileForkOffsetHFS', label: 'Attributes File Fork', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Volume Header Position + 0x160' }, { id: 'startupFileForkOffsetHFS', label: 'Startup File Fork', dependencies: ['partitionStartHFS', 'volumeHeaderOffsetHFS'], formula: 'Volume Header Position + 0x1B0' } ] }, { name: 'Catalog File (Special File)', results: [ { id: 'catalogFileStartHFS', label: 'Catalog File Start', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS'], formula: 'Partitionsstart + (Startblock × Block-Größe)' }, { id: 'catalogFileSizeBytesHFS', label: 'Catalog File Größe (Bytes)', dependencies: ['catalogSizeBlocksHFS', 'blockSizeHFS'], formula: 'Größe in Blöcken × Block-Größe' }, { id: 'catalogHeaderNodeHFS', label: 'Catalog Header Node (Knoten 0)', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS'], formula: 'Catalog File Start + (0 × Knoten-Größe)' }, { id: 'catalogRootNodeOffsetHFS', label: 'Catalog Wurzelknoten Offset', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS', 'catalogRootNodeHFS', 'catalogNodeSizeHFS'], formula: 'Catalog File Start + (Wurzelknoten-Nr. × Knoten-Größe)' } ] }, { name: 'B-Baum Knoten-Navigation', results: [ { id: 'targetNodeOffsetHFS', label: 'Ziel-Knoten Offset', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS', 'targetNodeNumberHFS', 'catalogNodeSizeHFS'], formula: 'Catalog File Start + (Knotennummer × Knoten-Größe)' }, { id: 'nodeDescriptorOffsetHFS', label: 'Node Descriptor (Knotenanfang)', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS', 'targetNodeNumberHFS', 'catalogNodeSizeHFS'], formula: 'Ziel-Knoten Offset + 0x00' }, { id: 'firstRecordOffsetHFS', label: 'Erster Eintrag (Record) Offset', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS', 'targetNodeNumberHFS', 'catalogNodeSizeHFS'], formula: 'Ziel-Knoten Offset + 0x0E' }, { id: 'offsetArrayStartHFS', label: 'Offset-Array Start (Knotenende)', dependencies: ['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS', 'targetNodeNumberHFS', 'catalogNodeSizeHFS'], formula: 'Ziel-Knoten Offset + Knoten-Größe - 0x02' } ] }, { name: 'Reservierte CNIDs (System-Dateien)', results: [ { id: 'rootParentCNIDHFS', label: 'CNID 0x01 - Eltern-CNID des Wurzelverzeichnisses', dependencies: [], formula: 'CNID = 0x01 (Spezialwert)' }, { id: 'rootDirCNIDHFS', label: 'CNID 0x02 - Wurzelverzeichnis', dependencies: [], formula: 'CNID = 0x02' }, { id: 'extentsOverflowCNIDHFS', label: 'CNID 0x03 - Extents Overflow File', dependencies: [], formula: 'CNID = 0x03' }, { id: 'catalogFileCNIDHFS', label: 'CNID 0x04 - Catalog File', dependencies: [], formula: 'CNID = 0x04' }, { id: 'badBlockCNIDHFS', label: 'CNID 0x05 - Bad Block File', dependencies: [], formula: 'CNID = 0x05' }, { id: 'allocationFileCNIDHFS', label: 'CNID 0x06 - Allocation File (Bitmap)', dependencies: [], formula: 'CNID = 0x06' }, { id: 'startupFileCNIDHFS', label: 'CNID 0x07 - Startup File', dependencies: [], formula: 'CNID = 0x07' }, { id: 'attributesFileCNIDHFS', label: 'CNID 0x08 - Attributes File', dependencies: [], formula: 'CNID = 0x08' }, { id: 'firstFreeCNIDHFS', label: 'CNID 0x10 - Erste frei verfügbare CNID', dependencies: [], formula: 'CNID = 0x10' } ] }, { name: 'Block-Berechnungen', results: [ { id: 'blockOffsetHFS', label: 'Block Offset', dependencies: ['partitionStartHFS', 'targetBlockNumberHFS', 'blockSizeHFS'], formula: 'Partitionsstart + (Block-Nummer × Block-Größe)' } ] }, { name: 'Fork-Berechnungen (Dateiinhalte)', results: [ { id: 'dataForkOffsetHFS', label: 'Data Fork Start Offset', dependencies: ['partitionStartHFS', 'dataForkStartBlockHFS', 'blockSizeHFS'], formula: 'Partitionsstart + (Startblock × Block-Größe)' }, { id: 'totalFileSizeHFS', label: 'Gesamte Dateigröße (Data + Resource Fork)', dependencies: ['dataForkSizeBytesHFS', 'resourceForkSizeBytesHFS'], formula: 'Data Fork Größe + Resource Fork Größe' } ] } ], formulas: [ { name: 'Volume Header Position', expression: 'Partitionsstart + 0x400 (immer fix bei Offset 0x400)' }, { name: 'Backup Volume Header', expression: 'Partitionsende - 0x200 (512 Bytes vor Partitionsende)' }, { name: 'Partitionsgröße', expression: 'Gesamtanzahl Blöcke × Block-Größe in Bytes' }, { name: 'Special File Fork Offsets', expression: 'Volume Header + fixer Offset (0x70, 0xC0, 0x110, 0x160, 0x1B0)' }, { name: 'Catalog File Start', expression: 'Partitionsstart + (Catalog-Startblock × Block-Größe)' }, { name: 'B-Baum Knoten Offset', expression: 'Start Special File + (Knotennummer × Knoten-Größe)' }, { name: 'Erster Eintrag im Knoten', expression: 'Knoten-Offset + 0x0E (nach Node Descriptor)' }, { name: 'Offset-Array (Eintragszeiger)', expression: 'Knotenende - ((Eintragsnummer + 1) × 2), dann 2 Bytes lesen für relatives Offset' }, { name: 'Block Offset', expression: 'Partitionsstart + (Block-Nummer × Block-Größe)' }, { name: 'Data Fork Offset', expression: 'Partitionsstart + (Fork-Startblock × Block-Größe)' }, { name: 'Gesamte Dateigröße', expression: 'Data Fork Größe + Resource Fork Größe' }, { name: 'Reservierte CNIDs', expression: 'CNID 0x01-0x08 für Systemdateien, 0x10 erste freie CNID' } ] } ]); } calculate(variantId) { if (variantId !== 'hfsplus') return; const values = this.getInputValues(variantId); // Validate input ranges specific to HFS+ filesystem const validation = this.validateFilesystemSpecific(variantId, values); if (!validation.valid) { console.warn('HFS+ validation errors:', validation.errors); // Continue with calculation but log warnings } const results = {}; this.calculateHFSPlus(values, results); } validateFilesystemSpecific(variantId, values) { if (variantId !== 'hfsplus') return { valid: true, errors: [] }; const errors = []; // Validate block size is reasonable (typically 4096 bytes, but can range from 512 to 1MB) if (values.blockSizeHFS !== undefined) { if (values.blockSizeHFS < 512 || values.blockSizeHFS > 1048576) { errors.push(`Block-Größe: ${values.blockSizeHFS} außerhalb akzeptabler Grenzen (512-1048576)`); } } // Validate CNID is not in reserved range if it's meant to be a user file if (values.targetCNIDHFS !== undefined) { if (values.targetCNIDHFS === 0) { errors.push('targetCNIDHFS: CNID 0 ist ungültig'); } } // Validate catalog node size is reasonable if (values.catalogNodeSizeHFS !== undefined) { if (values.catalogNodeSizeHFS < 512 || values.catalogNodeSizeHFS > 32768) { errors.push(`Knoten-Größe: ${values.catalogNodeSizeHFS} außerhalb akzeptabler Grenzen (512-32768)`); } } // Validate node number doesn't exceed reasonable bounds if (values.targetNodeNumberHFS !== undefined && values.catalogSizeBlocksHFS !== undefined && values.catalogNodeSizeHFS !== undefined && values.blockSizeHFS !== undefined) { const catalogSizeBytes = values.catalogSizeBlocksHFS * values.blockSizeHFS; const maxNodes = Math.floor(catalogSizeBytes / values.catalogNodeSizeHFS); if (values.targetNodeNumberHFS >= maxNodes) { errors.push(`targetNodeNumberHFS: ${values.targetNodeNumberHFS} >= maximale Knotenanzahl: ${maxNodes}`); } } return { valid: errors.length === 0, errors: errors }; } getInputValues(variantId) { const variant = this.variants.find(v => v.id === variantId); if (!variant) return {}; const values = {}; // Get all constants variant.constants.forEach(constant => { const input = document.getElementById(constant.id); if (input && input.value.trim()) { const val = parseInt(input.value.trim(), 16); if (!isNaN(val)) { values[constant.id] = val; } } else if (constant.default) { const val = parseInt(constant.default, 16); if (!isNaN(val)) { values[constant.id] = val; } } }); // Get all inputs variant.inputs.forEach(input => { const element = document.getElementById(input.id); if (element && element.value.trim()) { const val = parseInt(element.value.trim(), 16); if (!isNaN(val)) { values[input.id] = val; } } }); return values; } calculateHFSPlus(values, results) { const { partitionStartHFS = 0, volumeHeaderOffsetHFS = 0x400, blockSizeHFS, totalBlocksHFS, catalogStartBlockHFS, catalogSizeBlocksHFS, catalogRootNodeHFS, catalogNodeSizeHFS, targetNodeNumberHFS, targetBlockNumberHFS, dataForkSizeBytesHFS, dataForkStartBlockHFS, resourceForkSizeBytesHFS } = values; // Volume Header calculations if (checkDependencies(['partitionStartHFS', 'volumeHeaderOffsetHFS'])) { const volumeHeaderPos = partitionStartHFS + volumeHeaderOffsetHFS; results.volumeHeaderPosHFS = volumeHeaderPos; updateResultItem('volumeHeaderPosHFS', { bytes: volumeHeaderPos }, true, `0x${partitionStartHFS.toString(16).toUpperCase()} + 0x${volumeHeaderOffsetHFS.toString(16).toUpperCase()} = 0x${volumeHeaderPos.toString(16).toUpperCase()}`); } else { updateResultItem('volumeHeaderPosHFS', 0, false); } if (checkDependencies(['totalBlocksHFS', 'blockSizeHFS'])) { const partitionSizeBytes = totalBlocksHFS * blockSizeHFS; results.partitionSizeBytesHFS = partitionSizeBytes; updateResultItem('partitionSizeBytesHFS', { bytes: partitionSizeBytes }, true, `0x${totalBlocksHFS.toString(16).toUpperCase()} × 0x${blockSizeHFS.toString(16).toUpperCase()} = 0x${partitionSizeBytes.toString(16).toUpperCase()}`); // Backup Volume Header if (checkDependencies(['partitionStartHFS'])) { const backupVolumeHeaderPos = partitionStartHFS + partitionSizeBytes - 0x200; results.backupVolumeHeaderPosHFS = backupVolumeHeaderPos; updateResultItem('backupVolumeHeaderPosHFS', { bytes: backupVolumeHeaderPos }, true, `0x${partitionStartHFS.toString(16).toUpperCase()} + 0x${partitionSizeBytes.toString(16).toUpperCase()} - 0x200 = 0x${backupVolumeHeaderPos.toString(16).toUpperCase()}`); } else { updateResultItem('backupVolumeHeaderPosHFS', 0, false); } } else { updateResultItem('partitionSizeBytesHFS', 0, false); updateResultItem('backupVolumeHeaderPosHFS', 0, false); } // Special Files Fork Offsets (in Volume Header) if (results.volumeHeaderPosHFS !== undefined) { const vhPos = results.volumeHeaderPosHFS; results.allocationFileForkOffsetHFS = vhPos + 0x70; updateResultItem('allocationFileForkOffsetHFS', { bytes: results.allocationFileForkOffsetHFS }, true, `0x${vhPos.toString(16).toUpperCase()} + 0x70 = 0x${results.allocationFileForkOffsetHFS.toString(16).toUpperCase()}`); results.extentsFileForkOffsetHFS = vhPos + 0xC0; updateResultItem('extentsFileForkOffsetHFS', { bytes: results.extentsFileForkOffsetHFS }, true, `0x${vhPos.toString(16).toUpperCase()} + 0xC0 = 0x${results.extentsFileForkOffsetHFS.toString(16).toUpperCase()}`); results.catalogFileForkOffsetHFS = vhPos + 0x110; updateResultItem('catalogFileForkOffsetHFS', { bytes: results.catalogFileForkOffsetHFS }, true, `0x${vhPos.toString(16).toUpperCase()} + 0x110 = 0x${results.catalogFileForkOffsetHFS.toString(16).toUpperCase()}`); results.attributesFileForkOffsetHFS = vhPos + 0x160; updateResultItem('attributesFileForkOffsetHFS', { bytes: results.attributesFileForkOffsetHFS }, true, `0x${vhPos.toString(16).toUpperCase()} + 0x160 = 0x${results.attributesFileForkOffsetHFS.toString(16).toUpperCase()}`); results.startupFileForkOffsetHFS = vhPos + 0x1B0; updateResultItem('startupFileForkOffsetHFS', { bytes: results.startupFileForkOffsetHFS }, true, `0x${vhPos.toString(16).toUpperCase()} + 0x1B0 = 0x${results.startupFileForkOffsetHFS.toString(16).toUpperCase()}`); } else { updateResultItem('allocationFileForkOffsetHFS', 0, false); updateResultItem('extentsFileForkOffsetHFS', 0, false); updateResultItem('catalogFileForkOffsetHFS', 0, false); updateResultItem('attributesFileForkOffsetHFS', 0, false); updateResultItem('startupFileForkOffsetHFS', 0, false); } // Catalog File calculations if (checkDependencies(['partitionStartHFS', 'catalogStartBlockHFS', 'blockSizeHFS'])) { const catalogFileStart = partitionStartHFS + (catalogStartBlockHFS * blockSizeHFS); results.catalogFileStartHFS = catalogFileStart; updateResultItem('catalogFileStartHFS', { bytes: catalogFileStart }, true, `0x${partitionStartHFS.toString(16).toUpperCase()} + (0x${catalogStartBlockHFS.toString(16).toUpperCase()} × 0x${blockSizeHFS.toString(16).toUpperCase()}) = 0x${catalogFileStart.toString(16).toUpperCase()}`); if (checkDependencies(['catalogSizeBlocksHFS'])) { const catalogFileSizeBytes = catalogSizeBlocksHFS * blockSizeHFS; results.catalogFileSizeBytesHFS = catalogFileSizeBytes; updateResultItem('catalogFileSizeBytesHFS', { bytes: catalogFileSizeBytes }, true, `0x${catalogSizeBlocksHFS.toString(16).toUpperCase()} × 0x${blockSizeHFS.toString(16).toUpperCase()} = 0x${catalogFileSizeBytes.toString(16).toUpperCase()}`); } else { updateResultItem('catalogFileSizeBytesHFS', 0, false); } // Header Node (always node 0) results.catalogHeaderNodeHFS = catalogFileStart; updateResultItem('catalogHeaderNodeHFS', { bytes: catalogFileStart }, true, `0x${catalogFileStart.toString(16).toUpperCase()} + (0 × Knoten-Größe) = 0x${catalogFileStart.toString(16).toUpperCase()}`); // Root Node if (checkDependencies(['catalogRootNodeHFS', 'catalogNodeSizeHFS'])) { const catalogRootNodeOffset = catalogFileStart + (catalogRootNodeHFS * catalogNodeSizeHFS); results.catalogRootNodeOffsetHFS = catalogRootNodeOffset; updateResultItem('catalogRootNodeOffsetHFS', { bytes: catalogRootNodeOffset }, true, `0x${catalogFileStart.toString(16).toUpperCase()} + (0x${catalogRootNodeHFS.toString(16).toUpperCase()} × 0x${catalogNodeSizeHFS.toString(16).toUpperCase()}) = 0x${catalogRootNodeOffset.toString(16).toUpperCase()}`); } else { updateResultItem('catalogRootNodeOffsetHFS', 0, false); } // Target Node calculations if (checkDependencies(['targetNodeNumberHFS', 'catalogNodeSizeHFS'])) { const targetNodeOffset = catalogFileStart + (targetNodeNumberHFS * catalogNodeSizeHFS); results.targetNodeOffsetHFS = targetNodeOffset; updateResultItem('targetNodeOffsetHFS', { bytes: targetNodeOffset }, true, `0x${catalogFileStart.toString(16).toUpperCase()} + (0x${targetNodeNumberHFS.toString(16).toUpperCase()} × 0x${catalogNodeSizeHFS.toString(16).toUpperCase()}) = 0x${targetNodeOffset.toString(16).toUpperCase()}`); // Node Descriptor (always at start of node) results.nodeDescriptorOffsetHFS = targetNodeOffset; updateResultItem('nodeDescriptorOffsetHFS', { bytes: targetNodeOffset }, true, `0x${targetNodeOffset.toString(16).toUpperCase()} + 0x00 = 0x${targetNodeOffset.toString(16).toUpperCase()}`); // First Record (always at 0x0E after Node Descriptor) results.firstRecordOffsetHFS = targetNodeOffset + 0x0E; updateResultItem('firstRecordOffsetHFS', { bytes: results.firstRecordOffsetHFS }, true, `0x${targetNodeOffset.toString(16).toUpperCase()} + 0x0E = 0x${results.firstRecordOffsetHFS.toString(16).toUpperCase()}`); // Offset Array Start (at end of node) results.offsetArrayStartHFS = targetNodeOffset + catalogNodeSizeHFS - 0x02; updateResultItem('offsetArrayStartHFS', { bytes: results.offsetArrayStartHFS }, true, `0x${targetNodeOffset.toString(16).toUpperCase()} + 0x${catalogNodeSizeHFS.toString(16).toUpperCase()} - 0x02 = 0x${results.offsetArrayStartHFS.toString(16).toUpperCase()}`); } else { updateResultItem('targetNodeOffsetHFS', 0, false); updateResultItem('nodeDescriptorOffsetHFS', 0, false); updateResultItem('firstRecordOffsetHFS', 0, false); updateResultItem('offsetArrayStartHFS', 0, false); } } else { updateResultItem('catalogFileStartHFS', 0, false); updateResultItem('catalogFileSizeBytesHFS', 0, false); updateResultItem('catalogHeaderNodeHFS', 0, false); updateResultItem('catalogRootNodeOffsetHFS', 0, false); updateResultItem('targetNodeOffsetHFS', 0, false); updateResultItem('nodeDescriptorOffsetHFS', 0, false); updateResultItem('firstRecordOffsetHFS', 0, false); updateResultItem('offsetArrayStartHFS', 0, false); } // Reserved CNIDs (these are constants, just display them) updateResultItem('rootParentCNIDHFS', 0x01, true, 'CNID = 0x01 (Spezialwert für Eltern-CNID des Wurzelverzeichnisses)'); updateResultItem('rootDirCNIDHFS', 0x02, true, 'CNID = 0x02 (Wurzelverzeichnis)'); updateResultItem('extentsOverflowCNIDHFS', 0x03, true, 'CNID = 0x03 (Extents Overflow File)'); updateResultItem('catalogFileCNIDHFS', 0x04, true, 'CNID = 0x04 (Catalog File)'); updateResultItem('badBlockCNIDHFS', 0x05, true, 'CNID = 0x05 (Bad Block File)'); updateResultItem('allocationFileCNIDHFS', 0x06, true, 'CNID = 0x06 (Allocation File / Bitmap)'); updateResultItem('startupFileCNIDHFS', 0x07, true, 'CNID = 0x07 (Startup File)'); updateResultItem('attributesFileCNIDHFS', 0x08, true, 'CNID = 0x08 (Attributes File)'); updateResultItem('firstFreeCNIDHFS', 0x10, true, 'CNID = 0x10 (Erste frei verfügbare CNID für Benutzer-Dateien)'); // Block calculations if (checkDependencies(['partitionStartHFS', 'targetBlockNumberHFS', 'blockSizeHFS'])) { const blockOffset = partitionStartHFS + (targetBlockNumberHFS * blockSizeHFS); results.blockOffsetHFS = blockOffset; updateResultItem('blockOffsetHFS', { bytes: blockOffset }, true, `0x${partitionStartHFS.toString(16).toUpperCase()} + (0x${targetBlockNumberHFS.toString(16).toUpperCase()} × 0x${blockSizeHFS.toString(16).toUpperCase()}) = 0x${blockOffset.toString(16).toUpperCase()}`); } else { updateResultItem('blockOffsetHFS', 0, false); } // Fork calculations if (checkDependencies(['partitionStartHFS', 'dataForkStartBlockHFS', 'blockSizeHFS'])) { const dataForkOffset = partitionStartHFS + (dataForkStartBlockHFS * blockSizeHFS); results.dataForkOffsetHFS = dataForkOffset; updateResultItem('dataForkOffsetHFS', { bytes: dataForkOffset }, true, `0x${partitionStartHFS.toString(16).toUpperCase()} + (0x${dataForkStartBlockHFS.toString(16).toUpperCase()} × 0x${blockSizeHFS.toString(16).toUpperCase()}) = 0x${dataForkOffset.toString(16).toUpperCase()}`); } else { updateResultItem('dataForkOffsetHFS', 0, false); } if (checkDependencies(['dataForkSizeBytesHFS', 'resourceForkSizeBytesHFS'])) { const totalFileSize = dataForkSizeBytesHFS + resourceForkSizeBytesHFS; results.totalFileSizeHFS = totalFileSize; updateResultItem('totalFileSizeHFS', { bytes: totalFileSize }, true, `0x${dataForkSizeBytesHFS.toString(16).toUpperCase()} + 0x${resourceForkSizeBytesHFS.toString(16).toUpperCase()} = 0x${totalFileSize.toString(16).toUpperCase()}`); } else { updateResultItem('totalFileSizeHFS', 0, false); } } // Generate HTML for the big-endian warning generateTimestampConverterHTML(variantId) { if (variantId !== 'hfsplus') return ''; return `

⚠️ Wichtiger Hinweis: Big-Endian Byte-Reihenfolge

HFS+ verwendet Big-Endian für alle Multi-Byte-Werte!

`; } }