409 lines
27 KiB
JavaScript
409 lines
27 KiB
JavaScript
// 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 `
|
||
<div class="section info-section">
|
||
<h2>⚠️ Wichtiger Hinweis: Big-Endian Byte-Reihenfolge</h2>
|
||
<div class="info-box">
|
||
<p><strong>HFS+ verwendet Big-Endian für alle Multi-Byte-Werte!</strong></p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|