@@ -1641,6 +1641,7 @@ export class NVSEditor {
16411641 <div><strong>Status:</strong> <span class="${ status === "OK" ? "nvs-ok" : "nvs-warn" } ">${ status } </span></div>
16421642 <button class="nvs-blob-dump" data-key="${ this . _esc ( id ) } " title="Show hex dump">📄 Hex Dump</button>
16431643 <button class="nvs-blob-download" data-key="${ this . _esc ( id ) } " title="Download blob">⬇️ Download</button>
1644+ <button class="nvs-blob-upload" data-key="${ this . _esc ( id ) } " title="Upload / replace blob from file">⬆️ Upload</button>
16441645 </div>` ;
16451646 }
16461647 } else {
@@ -1735,5 +1736,158 @@ export class NVSEditor {
17351736 } , 0 ) ;
17361737 } ) ;
17371738 } ) ;
1739+
1740+ // Blob upload buttons — replace blob payload from a local file
1741+ dialogContainer . querySelectorAll ( ".nvs-blob-upload" ) . forEach ( ( btn ) => {
1742+ btn . addEventListener ( "click" , ( ) => {
1743+ const id = btn . dataset . key ;
1744+ const blob = blobs . get ( id ) ;
1745+ if ( ! blob ) return ;
1746+
1747+ const input = document . createElement ( "input" ) ;
1748+ input . type = "file" ;
1749+ input . style . display = "none" ;
1750+ document . body . appendChild ( input ) ;
1751+
1752+ input . addEventListener ( "change" , async ( ) => {
1753+ const file = input . files && input . files [ 0 ] ;
1754+ input . remove ( ) ;
1755+ if ( ! file ) return ;
1756+
1757+ try {
1758+ const buf = await file . arrayBuffer ( ) ;
1759+ const bytes = new Uint8Array ( buf ) ;
1760+
1761+ if (
1762+ ! confirm (
1763+ `Replace blob "${ blob . namespace } ::${ blob . key } " ` +
1764+ `(current ${ blob . totalSize } bytes) with ${ bytes . length } bytes ` +
1765+ `from "${ file . name } "?` ,
1766+ )
1767+ )
1768+ return ;
1769+
1770+ this . _uploadBlobToNVS ( blob , bytes ) ;
1771+
1772+ // Re-render the dialog to reflect the new blob state
1773+ dialogContainer . remove ( ) ;
1774+ this . _showBlobs ( ) ;
1775+ } catch ( e ) {
1776+ alert ( "Upload failed: " + ( e && e . message ? e . message : e ) ) ;
1777+ }
1778+ } ) ;
1779+
1780+ input . click ( ) ;
1781+ } ) ;
1782+ } ) ;
1783+ }
1784+
1785+ /**
1786+ * Replace the data of an existing blob in the NVS partition with `fileBytes`.
1787+ * Distributes bytes across the existing data-chunk slots (0x42 entries),
1788+ * updates each entry's size + data-CRC + header-CRC, and refreshes the
1789+ * blob_index totalSize (0x48) when present. Will not allocate new chunks
1790+ * or extend the partition; the new payload must fit in the existing slots.
1791+ */
1792+ _uploadBlobToNVS ( blob , fileBytes ) {
1793+ // Resolve the parsed data-chunk items (in chunk-index order) so we know
1794+ // each slot's offset/span and can size-check before mutating anything.
1795+ const sortedChunks = [ ...blob . chunks ] . sort ( ( a , b ) => a . index - b . index ) ;
1796+ if ( sortedChunks . length === 0 ) {
1797+ throw new Error ( "No data chunks found for this blob." ) ;
1798+ }
1799+
1800+ const dataEntries = [ ] ;
1801+ for ( const chunk of sortedChunks ) {
1802+ // chunk.offset points to the payload; the entry header is the 32 bytes before.
1803+ const entryOff = chunk . offset - 32 ;
1804+ const item = this . _findItem ( entryOff ) ;
1805+ if ( ! item || item . datatype !== 0x42 ) {
1806+ throw new Error (
1807+ `Could not locate data entry at 0x${ entryOff . toString ( 16 ) } .` ,
1808+ ) ;
1809+ }
1810+ // span 1 = inline (no payload extension); capacity is (span-1)*32 bytes.
1811+ const maxSize = ( item . span - 1 ) * 32 ;
1812+ dataEntries . push ( { item, maxSize } ) ;
1813+ }
1814+
1815+ const totalCapacity = dataEntries . reduce ( ( s , e ) => s + e . maxSize , 0 ) ;
1816+ if ( fileBytes . length > totalCapacity ) {
1817+ throw new Error (
1818+ `File too large: ${ fileBytes . length } bytes, but only ${ totalCapacity } bytes ` +
1819+ `available across ${ dataEntries . length } chunk slot(s). ` +
1820+ `Adding new chunks is not supported.` ,
1821+ ) ;
1822+ }
1823+
1824+ // Distribute payload across chunks sequentially.
1825+ let writeOffset = 0 ;
1826+ for ( const { item, maxSize } of dataEntries ) {
1827+ const off = item . offset ;
1828+ const dataOff = off + 32 ;
1829+ const remaining = fileBytes . length - writeOffset ;
1830+ const writeLen = Math . min ( remaining , maxSize ) ;
1831+
1832+ // Wipe the old payload area, then write the new segment.
1833+ this . data . fill ( 0xff , dataOff , dataOff + maxSize ) ;
1834+ if ( writeLen > 0 ) {
1835+ this . data . set (
1836+ fileBytes . subarray ( writeOffset , writeOffset + writeLen ) ,
1837+ dataOff ,
1838+ ) ;
1839+ }
1840+
1841+ // Update size field at +24 (u16) — the high two bytes are unused (0xFF).
1842+ this . data [ off + 24 ] = writeLen & 0xff ;
1843+ this . data [ off + 25 ] = ( writeLen >> 8 ) & 0xff ;
1844+ this . data [ off + 26 ] = 0xff ;
1845+ this . data [ off + 27 ] = 0xff ;
1846+
1847+ // Update data CRC at +28 (u32 little-endian).
1848+ const segData = this . data . subarray ( dataOff , dataOff + writeLen ) ;
1849+ const dataCrc = NVSEditor . crc32 ( segData , 0 , writeLen ) ;
1850+ const dv = new DataView (
1851+ this . data . buffer ,
1852+ this . data . byteOffset + off + 28 ,
1853+ 4 ,
1854+ ) ;
1855+ dv . setUint32 ( 0 , dataCrc >>> 0 , true ) ;
1856+
1857+ // Recalculate header CRC at +4.
1858+ const hcrc = NVSEditor . crc32Header ( this . data , off ) ;
1859+ const hdv = new DataView (
1860+ this . data . buffer ,
1861+ this . data . byteOffset + off + 4 ,
1862+ 4 ,
1863+ ) ;
1864+ hdv . setUint32 ( 0 , hcrc >>> 0 , true ) ;
1865+
1866+ writeOffset += writeLen ;
1867+ }
1868+
1869+ // If a blob_index (0x48) entry exists, sync its totalSize at +24 (u32) and
1870+ // recalc its header CRC. chunkCount stays the same — we did not add slots.
1871+ if ( blob . indexEntry && blob . indexEntry . datatype === 0x48 ) {
1872+ const idxOff = blob . indexEntry . offset ;
1873+ const dv = new DataView (
1874+ this . data . buffer ,
1875+ this . data . byteOffset + idxOff + 24 ,
1876+ 4 ,
1877+ ) ;
1878+ dv . setUint32 ( 0 , fileBytes . length >>> 0 , true ) ;
1879+ const hcrc = NVSEditor . crc32Header ( this . data , idxOff ) ;
1880+ const hdv = new DataView (
1881+ this . data . buffer ,
1882+ this . data . byteOffset + idxOff + 4 ,
1883+ 4 ,
1884+ ) ;
1885+ hdv . setUint32 ( 0 , hcrc >>> 0 , true ) ;
1886+ }
1887+
1888+ this . modified = true ;
1889+ this . pages = this . _parse ( ) ;
1890+ this . _renderContent ( ) ;
1891+ this . _updateWriteButton ( ) ;
17381892 }
17391893}
0 commit comments