Tot el mòdul hem estat envoltats de Buffer sense mirar-los de cara. Quan escrivim fs.readFile(FITXER_ESDEVENIMENTS, 'utf8') demanem una traducció; quan l'ometem —en copiar un cartell, en comprimir l'històric amb zlib— el que viatjava pel codi era un Buffer pur. Els trossos d'un stream binari són Buffer. La resposta d'una petició HTTP del Mòdul 4 arribarà en Buffer. Un hash del Mòdul 8 és un Buffer.
Aquesta lliçó obre la caixa: què és exactament un Buffer i per què Node el va haver d'inventar, com es crea sense deixar forats de seguretat, què significa cada codificació, com es llegeixen números binaris i per què importa l'ordre dels seus bytes. Veuràs també els dos errors clàssics de tractar bytes com si fossin caràcters: un subarray que comparteix memòria amb l'original i un emoji partit per la meitat entre dos trossos d'un stream. I ho aplicarem a Escena Viva: comprovar que el cartell que puja un organitzador és de debò un PNG —mirant els seus primers bytes, no la seva extensió— i generar la càrrega útil del codi QR d'una entrada en base64url, signada i comparable en temps constant.
Contingut
- Què és un
Bufferi per què existeix - Crear buffers:
from,alloci el perill d'allocUnsafe - Codificacions i per a què serveix cadascuna
- Números binaris i ordre de bytes
- Operacions:
subarray,concat,copy,compare,indexOf - Bytes davant de caràcters: la longitud enganyosa
StringDecoder: no partir un caràcter en dos- Escena Viva: validar el cartell pels seus nombres màgics
- Escena Viva: el QR d'una entrada en
base64url - Comparar secrets en temps constant
Buffer,TypedArrayiArrayBuffer
- Què és un
Buffer i per què existeix
Buffer i per què existeixJavaScript va néixer al navegador per manipular text i documents. Fins al 2011 no tenia cap tipus capaç de representar dades binàries: només cadenes UTF-16, números en coma flotant i objectes. Node, en canvi, va néixer per llegir fitxers i parlar per sòcols, és a dir, per moure bytes. Necessitava un tipus que el llenguatge no li donava, i el va crear: Buffer.
Un Buffer és una seqüència de bytes de longitud fixa: cada posició guarda un enter entre 0 i 255, i no pot créixer ni encongir-se —per tenir-ne un de més gran cal crear-ne un altre i copiar—. Dos trets el distingeixen d'un array normal:
- És una
Uint8Array. Literalment:Buffer.prototypehereta d'Uint8Array.prototype. Tot el que funciona sobre unaUint8Arrayfunciona sobre unBuffer, més els mètodes que Node hi afegeix a sobre (toString,write,readUInt32BE…). - La seva memòria viu fora del munt de V8. L'objecte
Bufferque manipules sí que és al munt, però els bytes són en memòria reservada a part. Això permet que el sistema operatiu hi escrigui directament sense copiar, i que un procés mogui centenars de megabytes sense pressionar el recol·lector d'escombraries. És també la raó que a la lliçó 03-04heapUsedamb prou feines es mogués mentre quersssí que creixia.
const buf = Buffer.from('Teatro Almendra', 'utf8');
console.log(buf instanceof Uint8Array, buf.length, buf[0]); // true 15 84 <-- bytes, no caracters
console.log(buf.toString('hex')); // 5465617472...
console.log(buf); // <Buffer 54 65 61 74 72 6f 20 ...>Fixa't en com ho imprimeix la consola: <Buffer ...> seguit dels bytes en hexadecimal. Si veus això als teus registres on esperaves text, la causa és gairebé sempre la mateixa: has oblidat la codificació en llegir.
- Crear buffers:
from, alloc i el perill d'allocUnsafe
from, alloc i el perill d'allocUnsafe| Forma | Què fa | Quan fer-la servir |
|---|---|---|
Buffer.from(cadena, cod) |
Codifica el text a bytes | Convertir text a binari |
Buffer.from([1, 2, 3]) |
Un byte per element (& 255) |
Signatures i dades literals |
Buffer.from(altreBuffer) |
Copia el contingut | Aïllar una còpia independent |
Buffer.from(arrayBuffer) |
Comparteix memòria, no copia | Interoperar amb TypedArray |
Buffer.alloc(n) |
n bytes posats a zero |
Per defecte, sempre |
Buffer.allocUnsafe(n) |
n bytes sense inicialitzar |
Només si el sobreescriuràs sencer |
Buffer.concat([a, b]) |
Uneix uns quants en un de nou | Reconstruir un stream acumulat |
Compte amb les files tercera i quarta, que semblen intercanviables i no ho són: Buffer.from(altreBuffer) copia, mentre que Buffer.from(arrayBuffer) comparteix memòria, així que modificar-ne un afectaria l'altre només en el segon cas.
El nom allocUnsafe no és cap exageració de la documentació. Buffer.alloc(n) recorre els n bytes posant-los a zero; allocUnsafe(n) els lliura tal com estaven a la memòria reservada, que pot contenir restes de dades anteriors del mateix procés: fragments d'un JSON llegit fa un segon, trossos d'una contrasenya, mitja capçalera HTTP.
Buffer.alloc(64).fill('token-de-sessio-abc123'); // embrutem memoria i la deixem anar
console.log(Buffer.alloc(64).toString('hex').slice(0, 24)); // 000000000000000000000000
console.log(Buffer.allocUnsafe(64).toString('latin1').slice(0, 40)); // brossa, de vegades recognoscibleEl 2018 aquesta diferència va produir una família sencera de fuites d'informació en paquets d'npm que reservaven un buffer amb allocUnsafe i l'enviaven per xarxa sense omplir-lo del tot: els bytes sobrants viatjaven amb el que hi hagués abans. La regla pràctica és simple: fes servir sempre Buffer.alloc; allocUnsafe només quan la línia següent hagi de sobreescriure el buffer complet i el rendiment estigui mesurat i justificat.
- Codificacions i per a què serveix cadascuna
Una codificació és un contracte de traducció entre bytes i caràcters. Node admet aquestes:
| Codificació | Bytes per caràcter | Alfabet de sortida | Ús típic |
|---|---|---|---|
utf8 |
1 a 4 | Tot Unicode | Per defecte per a text |
utf16le |
2 o 4 | Tot Unicode | Interoperar amb Windows/API natives |
latin1 |
Sempre 1 | 0–255 | Capçaleres binàries, byte↔caràcter 1:1 |
ascii |
1 (descarta el bit alt) | 0–127 | Gairebé mai: corromp accents |
hex |
2 caràcters per byte | 0-9a-f |
Hashes, bolcats, depuració |
base64 |
~1,33 caràcters per byte | A-Za-z0-9+/= |
Binari dins de JSON o correu |
base64url |
~1,33 caràcters per byte | A-Za-z0-9-_ |
Binari dins d'URL (sense farciment) |
const text = 'Sessio al Teatro Almendra';
const buf = Buffer.from(text, 'utf8');
console.log(buf.toString('base64')); // U2Vzc2lvIGFsIFRlYXRybyBBbG1lbmRyYQ==
console.log(buf.toString('base64url')); // U2Vzc2lvIGFsIFRlYXRybyBBbG1lbmRyYQ
console.log(buf.toString('hex').slice(0, 8)); // 53657373Dues precisions importants:
base64no és xifratge. És una representació reversible sense cap secret. Serveix per ficar bytes on només caben caràcters imprimibles, no per protegir res.base64urlés la variant segura per a URL: substitueix+per-,/per_i elimina el farciment=. És exactament el que fan servir els JWT del Mòdul 8 i el que farem servir per al QR de les entrades, perquè un+dins d'una URL s'interpreta com un espai i una/parteix la ruta.- Una codificació desconeguda no falla en silenci:
Buffer.from(x, 'utf-9')llançaTypeError: Unknown encoding, i pots consultar la llista ambBuffer.isEncoding('base64url').
- Números binaris i ordre de bytes
Un format binari no guarda «1189» com a text: guarda el número en un nombre fix de bytes. Llegir-lo requereix saber quants bytes ocupa, si té signe i en quin ordre estan els seus bytes. Aquest últim punt és l'ordre de bytes (endianness):
- BE (big endian): el byte més significatiu primer. És l'ordre de les xarxes i de la majoria de formats de fitxer (PNG, JPEG).
- LE (little endian): el menys significatiu primer. És l'ordre natiu dels processadors x86 i ARM habituals.
const buf = Buffer.alloc(4);
buf.writeUInt32BE(1189, 0); // les entrades lliures de la llavor
console.log(buf); // <Buffer 00 00 04 a5>
console.log(buf.readUInt32BE(0)); // 1189
console.log(buf.readUInt32LE(0)); // 2768994304 <-- mateixos bytes, altre ordre
console.log(require('node:os').endianness()); // 'LE' a la majoria de maquinesEls mateixos quatre bytes valen 1189 o 2768994304 segons com els interpretis. Per això l'ordre no s'endevina: el fixa el format i cal llegir-ne l'especificació. Els mètodes segueixen tots el mateix patró: read/write + U si no té signe + Int + mida en bits + BE/LE. Per a 64 bits existeixen readBigUInt64BE i companyia, que tornen BigInt. Escriure un valor fora de rang o en un desplaçament que se surt del buffer llança ERR_OUT_OF_RANGE: és un error sorollós, no una corrupció silenciosa.
- Operacions:
subarray, concat, copy, compare, indexOf
subarray, concat, copy, compare, indexOf| Operació | Què fa | Trampa |
|---|---|---|
buf.subarray(ini, fi) |
Vista sobre el mateix buffer | Comparteix memòria |
buf.slice(ini, fi) |
Àlies de subarray (obsolet) |
No copia, al contrari que als arrays |
Buffer.concat([a, b], n) |
Buffer nou amb tot | Copia: costa memòria |
origen.copy(desti, dOff) |
Copia bytes a un altre buffer | La destinació ha de tenir lloc |
a.equals(b) / a.compare(b) |
Igualtat / ordre | compare torna -1, 0 o 1 |
buf.indexOf('EV-') |
Cerca bytes o text | Torna posició en bytes |
buf.fill(valor) |
Omple tot el buffer | Modifica al lloc |
L'error clàssic és a la primera fila. En un array, slice torna una còpia; en un Buffer, slice i subarray tornen una finestra sobre la mateixa memòria:
const original = Buffer.from('EV-2026-000123', 'utf8');
const any = original.subarray(3, 7); // 2026
any.write('1999'); // sembla inofensiu...
console.log(original.toString()); // EV-1999-000123 <-- l'original ha canviatSi necessites una còpia de debò, demana-la explícitament: Buffer.from(original.subarray(3, 7)) o Buffer.copyBytesFrom(...). Aquest detall és la causa d'una fallada molt difícil de trobar: guardar trossos d'un stream amb trossos.push(dades) sense copiar-los, quan l'stream reutilitza el mateix buffer intern entre lectures; els trossos acumulats acaben contenint tots el mateix, l'últim. Quant a concat, és la manera correcta de reconstruir un contingut acumulat —Buffer.concat(trossos) després d'un for await sobre un stream— i accepta un tercer argument amb la longitud total, que evita recalcular-la si ja la coneixes. Aquest patró és exactament el que fa readFile per dins, amb la mateixa conseqüència de memòria que vam mesurar a 03-04: fes-lo servir només quan sàpigues que el contingut és petit.
- Bytes davant de caràcters: la longitud enganyosa
String.length compta unitats UTF-16. Buffer.length compta bytes. I Buffer.byteLength(cadena) et diu quants bytes ocuparà un text abans de convertir-lo. Els tres números poden ser diferents:
for (const text of ['Almendra', 'Boveda', 'Bóveda', 'Monólogos', '🎭']) {
console.log(text.padEnd(10),
`String.length=${text.length}`,
`bytes=${Buffer.byteLength(text, 'utf8')}`,
`caracters=${[...text].length}`);
}Almendra String.length=8 bytes=8 caracters=8 Boveda String.length=6 bytes=6 caracters=6 Bóveda String.length=6 bytes=7 caracters=6 Monólogos String.length=9 bytes=10 caracters=9 🎭 String.length=2 bytes=4 caracters=1
Tres lliçons d'aquesta taula. Primera: una ó ocupa dos bytes en UTF-8, així que reservar un buffer amb Buffer.alloc(text.length) per guardar text accentuat el trunca. Segona: un emoji ocupa dues unitats a String.length i una en iterar-lo amb [...text], perquè JavaScript representa els caràcters fora del pla bàsic com una parella de valors substituts. Tercera, i la que trenca programes de debò: tallar un buffer per una posició arbitrària pot partir un caràcter en dos.
const sala = Buffer.from('Sala Bóveda', 'utf8'); // 12 bytes
const part1 = sala.subarray(0, 7); // talla dins de la 'ó'
const part2 = sala.subarray(7);
console.log(part1.toString('utf8')); // Sala B� <-- caracter de reemplacament
console.log(part1.toString() + part2.toString()); // Sala B��veda <-- irrecuperable
console.log(Buffer.concat([part1, part2]).toString()); // Sala Bóveda <-- correcteConvertir cada tros a text per separat destrueix el caràcter partit: el � (U+FFFD) ja no es pot desfer. Unir primer els bytes i descodificar després funciona, però exigeix tenir tot el contingut a la memòria, que és justament el que un stream evita.
StringDecoder: no partir un caràcter en dos
StringDecoder: no partir un caràcter en dosEl problema anterior no és teòric: és exactament el que passa quan un stream lliura trossos de 64 KB i el caràcter número 65.536 cau a cavall entre dos. La solució és a node:string_decoder, un descodificador amb estat que reté els bytes incomplets del final d'un tros fins que arriben els que falten.
const { StringDecoder } = require('node:string_decoder');
const descodificador = new StringDecoder('utf8');
const sala = Buffer.from('Sala Bóveda', 'utf8');
console.log(JSON.stringify(descodificador.write(sala.subarray(0, 7)))); // "Sala B"
console.log(JSON.stringify(descodificador.write(sala.subarray(7)))); // "óveda"
console.log(JSON.stringify(descodificador.end())); // ""El primer write torna "Sala B" sense la ó: el descodificador ha vist el primer byte del caràcter i se l'ha guardat. El segon write el completa i emet óveda. L'end() final torna el que quedés pendent —si el flux acaba amb un caràcter incomplet, allà apareix el �, senyal que l'entrada estava truncada—. Quan passes { encoding: 'utf8' } a createReadStream o crides setEncoding('utf8') sobre un stream, Node fa servir internament un StringDecoder, i per això els trossos ja arriben com a text correcte. Necessites fer-lo servir a mà només quan treballes amb els Buffer crus: en desxifrar, en descomprimir per trossos o en implementar un protocol propi.
- Escena Viva: validar el cartell pels seus nombres màgics
Els organitzadors pugen el cartell del seu esdeveniment. L'extensió del fitxer no demostra res: reanomenar virus.exe a cartell.png costa un segon. El que sí que identifica un format són els seus primers bytes, l'anomenada signatura o nombre màgic.
| Format | Bytes inicials (hex) | Interpretació |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A |
.PNG + salts de control |
| JPEG | FF D8 FF |
Marcador d'inici d'imatge |
25 50 44 46 2D |
%PDF- |
|
| WEBP | 52 49 46 46 … 57 45 42 50 |
RIFF + WEBP al byte 8 |
| GZIP | 1F 8B |
El .gz que vam generar a 03-05 |
Aprofitem l'API FileHandle de la lliçó 03-02 per llegir només els primers bytes, sense carregar una imatge de deu megues per mirar-ne vuit:
// src/utils/tipus-fitxer.js
// Detecta el format real d'un fitxer pels seus primers bytes (nombre magic),
// mai per la seva extensio: l'extensio la tria qui puja el fitxer.
const fs = require('node:fs/promises');
const BYTES_CAPCALERA = 16;
const SIGNATURES = [
{ tipus: 'png', extensio: '.png', desplacament: 0, signatura: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) },
{ tipus: 'jpeg', extensio: '.jpg', desplacament: 0, signatura: Buffer.from([0xff, 0xd8, 0xff]) },
{ tipus: 'pdf', extensio: '.pdf', desplacament: 0, signatura: Buffer.from('%PDF-', 'latin1') },
{ tipus: 'gzip', extensio: '.gz', desplacament: 0, signatura: Buffer.from([0x1f, 0x8b]) },
{ tipus: 'webp', extensio: '.webp', desplacament: 8, signatura: Buffer.from('WEBP', 'latin1') }
];
// Llegeix els primers bytes del fitxer i torna nomes els que existeixin de veritat.
async function llegirCapcalera(ruta, bytes = BYTES_CAPCALERA) {
const gestor = await fs.open(ruta, 'r');
try {
const desti = Buffer.alloc(bytes);
const { bytesRead } = await gestor.read(desti, 0, bytes, 0);
return Buffer.from(desti.subarray(0, bytesRead));
} finally {
await gestor.close();
}
}
function detectarTipus(capcalera) {
for (const { tipus, extensio, desplacament, signatura } of SIGNATURES) {
if (capcalera.subarray(desplacament, desplacament + signatura.length).equals(signatura)) {
return { tipus, extensio };
}
}
return null;
}
async function validarCartell(ruta, tipusAcceptats = ['png', 'jpeg']) {
const capcalera = await llegirCapcalera(ruta);
const detectat = detectarTipus(capcalera);
if (detectat === null || !tipusAcceptats.includes(detectat.tipus)) {
const error = new Error(`El cartell ${ruta} no es un fitxer ${tipusAcceptats.join(' ni ')}`);
error.codi = 'FORMAT_NO_ACCEPTAT';
error.capcalera = capcalera.subarray(0, 8).toString('hex');
throw error;
}
return detectat;
}
module.exports = { llegirCapcalera, detectarTipus, validarCartell, SIGNATURES, BYTES_CAPCALERA };Quatre decisions que mereixen comentari: Buffer.alloc i no allocUnsafe, perquè si el fitxer té menys de 16 bytes els sobrants serien brossa de memòria comparada contra signatures reals; Buffer.from(desti.subarray(...)), que torna una còpia independent en comptes d'una vista amb accés als bytes sobrants; equals en comptes de toString, perquè comparar bytes amb bytes evita qualsevol dubte de codificació —0x89 no és text vàlid en UTF-8—; i error.codi seguint la convenció de domini del curs, amb la capçalera en hex adjunta per diagnosticar què va pujar realment l'organitzador.
node -e "require('./src/utils/tipus-fitxer.js').validarCartell('dades/esdeveniments.json').catch((e) => console.error(e.codi, e.capcalera))"
# FORMAT_NO_ACCEPTAT 7b0a20202265 <-- 0x7b es '{': un JSON disfressat de cartellLa comprovació funciona. Compte amb l'abast d'aquesta tècnica: la signatura demostra el format, no que el contingut sigui inofensiu. Al Mòdul 4, en rebre pujades de debò, la combinarem amb un límit de mida i amb la ruta blindada de la lliçó 03-03.
- Escena Viva: el QR d'una entrada en
base64url
base64urlCada entrada d'Escena Viva porta un codi EV-2026-000123 i un QR que l'acomodador escaneja a la porta. El QR conté una URL, i dins d'aquesta URL viatja una càrrega útil: les dades de l'entrada més una signatura que impedeix fabricar-ne. Aquest contingut és binari i ha de cabre en una URL sense escapaments: el cas exacte de base64url.
// src/utils/qr-entrada.js
// Carrega util del codi QR d'una entrada: cos en base64url i signatura HMAC.
// El format es <cos>.<signatura>, el mateix esquema que veurem amb JWT.
const crypto = require('node:crypto');
const SECRET = process.env.ESCENA_VIVA_SECRET ?? 'secret-de-desenvolupament';
const signar = (cos) => crypto.createHmac('sha256', SECRET).update(cos).digest('base64url');
function fallar(codi, missatge) {
const error = new Error(missatge);
error.codi = codi;
throw error;
}
function codificarQr({ codiEntrada, sessioId, emesaEl }) {
const carrega = JSON.stringify({ codiEntrada, sessioId, emesaEl });
const cos = Buffer.from(carrega, 'utf8').toString('base64url');
return `${cos}.${signar(cos)}`;
}
function descodificarQr(text) {
const [cos, signatura] = String(text).split('.');
if (cos === undefined || signatura === undefined) fallar('QR_MALFORMAT', "El QR no te el format <cos>.<signatura>");
if (!sonIguals(signatura, signar(cos))) fallar('QR_SIGNATURA_INVALIDA', 'La signatura del codi QR no es valida');
return JSON.parse(Buffer.from(cos, 'base64url').toString('utf8'));
}
module.exports = { codificarQr, descodificarQr };const qr = codificarQr({ codiEntrada: 'EV-2026-000123', sessioId: 'ses-001-1', emesaEl: '2026-08-14T19:30:00' });
console.log(qr); // eyJjb2RpRW50cmFkYSI6IkVWLTIwMjYtMDAwMTIzIiwic2Vzc2lvSWQiOiJz...Yk9u
console.log(descodificarQr(qr).sessioId); // ses-001-1
descodificarQr(qr.replace(/.$/, 'X')); // llanca QR_SIGNATURA_INVALIDAsonIguals és la funció de comparació segura de l'apartat següent, que viu en aquest mateix mòdul. El cos no està xifrat —qualsevol el pot descodificar amb Buffer.from(cos, 'base64url').toString()—, i això és acceptable: al QR d'una entrada no hi ha res secret. El que protegeix la signatura és la integritat: sense conèixer el secret no es pot fabricar una entrada vàlida ni canviar-li la sessió. La comparació d'aquesta signatura, però, no es pot fer amb ===, i aquest és l'últim apartat tècnic de la lliçó.
- Comparar secrets en temps constant
a === b sobre cadenes compara caràcter a caràcter i s'atura al primer que difereix. Aquesta optimització, inofensiva en qualsevol altre context, filtra informació quan el que es compara és un secret: un atacant que mesura el temps de resposta pot deduir quants caràcters inicials ha encertat i reconstruir la signatura byte a byte.
crypto.timingSafeEqual compara sempre tots els bytes, trigui el que trigui a trobar la primera diferència. Té una exigència: els dos buffers han de mesurar exactament el mateix, o llança ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, i la longitud de la dada aliena no la controles. La solució estàndard és normalitzar la longitud amb un hash abans de comparar:
// Afegir a src/utils/qr-entrada.js
function sonIguals(a, b) {
// El hash iguala les longituds (32 bytes sempre) sense filtrar informacio.
const resumA = crypto.createHash('sha256').update(String(a)).digest();
const resumB = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(resumA, resumB);
}digest() sense argument torna un Buffer —amb 'hex' tornaria una cadena—, i dos SHA-256 mesuren 32 bytes cadascun passi el que passi. Reprendrem aquesta funció tal qual al Mòdul 8, on el mateix raonament s'aplica a claus d'API i a testimonis de sessió.
Buffer, TypedArray i ArrayBuffer
Buffer, TypedArray i ArrayBufferLes tres peces encaixen així: un ArrayBuffer és un bloc de memòria en brut que no es pot llegir ni escriure directament; un TypedArray (Uint8Array, Int16Array, Float64Array…) és una vista que interpreta aquest bloc com a números de cert tipus, i diverses vistes poden mirar el mateix bloc; un Buffer és una Uint8Array amb mètodes afegits per Node.
const buf = Buffer.from('Ribera', 'utf8');
console.log(buf.byteOffset, buf.byteLength); // p. ex. 88 6 <-- compte amb l'offset
console.log(new Uint8Array(buf.buffer).length); // 8192 <-- NO son 6Aquí hi ha la trampa més subtil del mòdul. Node reserva els buffers petits (menys de 4 KB) dins d'un pool compartit de 8 KB, així que buf.buffer no és la memòria del teu buffer: és la del pool sencer, i buf.byteOffset indica on comença la teva porció. Passar buf.buffer a una API que espera les dades exactes lliura 8 KB de memòria aliena. La manera correcta de convertir:
const vista = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); // comparteix memoria
const altre = Buffer.from(vista.buffer, vista.byteOffset, vista.byteLength); // i la tornadaA la pràctica això apareix en interoperar amb API d'estàndard web dins de Node —fetch, crypto.subtle, WebSocket, els worker threads del Mòdul 10—, que parlen Uint8Array i ArrayBuffer, no Buffer.
Errors Comuns i Consells
- Veure
<Buffer 7b 0a ...>on esperaves text. Falta la codificació:readFile(ruta, 'utf8')o.toString('utf8'). No facis servir maiString(buf)amb l'esperança que faci el correcte amb dades binàries. - Fer servir
allocUnsafeper costum. Si no omples el buffer sencer, publiques memòria antiga del procés.allocper defecte, sempre. - Creure que
slicecopia. Comparteix memòria, al revés que als arrays. Còpia explícita ambBuffer.from(vista)si l'original ha de canviar o reutilitzar-se. - Reservar amb
text.lengthen comptes deBuffer.byteLength(text). Amb un sol accent ja et falta un byte i el text surt truncat. - Concatenar trossos com a cadenes.
tros.toString()per cada tros d'un stream parteix els caràcters multibyte: acumulaBufferi descodifica al final, o fes servirStringDecoder. - Comparar signatures amb
===. Filtra informació per temps.timingSafeEqualsobre buffers d'igual longitud, normalitzada amb un hash. - Consell: treballa amb
Bufferfins a l'últim moment possible i converteix a text una sola vegada, a la frontera del teu sistema. Cada conversió d'anada i tornada costa CPU i és una oportunitat de corrompre dades.
Exercicis
Exercici 1: inventari de cartells
Escriu src/laboratori/inventari-cartells.js que recorri un directori (per defecte dades/cartells/) i, per cada fitxer, imprimeixi en una taula el seu nom, la seva extensió declarada, el tipus real detectat amb detectarTipus i si coincideixen. Ha de marcar clarament els fitxers l'extensió dels quals menteix. Fes servir readdir amb withFileTypes de la lliçó 03-02 i les rutes de src/config/rutes.js.
Exercici 2: trossejador segur de text
Escriu una funció trossejarText(text, bytesMaxims) que torni un array de Buffer de com a màxim bytesMaxims bytes cadascun sense partir cap caràcter. Verifica amb 'Noche de Monólogos 🎭 en la Sala Bóveda' i bytesMaxims = 10 que en concatenar els trossos i descodificar recuperes el text original i que cap tros no produeix � per separat.
Exercici 3: capçalera binària de l'informe
Dissenya un format binari mínim per a l'informe diari d'ocupació: 4 bytes de signatura 'EVIV', 1 byte de versió, 2 bytes big endian amb el nombre de sessions, 4 bytes big endian amb les entrades venudes i la resta en JSON UTF-8. Escriu escriureInformeBinari(ruta, informe) i llegirInformeBinari(ruta), i comprova que el cicle complet torna les dades originals i que un fitxer amb signatura incorrecta llança un error amb codi: 'FORMAT_NO_ACCEPTAT'.
Solucions
Solució 1. La comparació entre extensió declarada i tipus real és el nucli:
const { readdir } = require('node:fs/promises');
const path = require('node:path');
const { llegirCapcalera, detectarTipus } = require('../utils/tipus-fitxer.js');
async function inventariar(directori) {
const files = [];
for (const entrada of await readdir(directori, { withFileTypes: true })) {
if (!entrada.isFile()) continue;
const detectat = detectarTipus(await llegirCapcalera(path.join(directori, entrada.name)));
const declarada = path.extname(entrada.name).toLowerCase();
const normalitzada = declarada === '.jpeg' ? '.jpg' : declarada; // .jpeg i .jpg son legitimes
files.push({ fitxer: entrada.name, declarada, real: detectat?.tipus ?? 'desconegut', coincideix: detectat?.extensio === normalitzada });
}
console.table(files);
return files;
}El cas .jpeg/.jpg recorda que un format pot tenir diverses extensions legítimes: la comprovació no ha de generar falses alarmes. Un desconegut no sempre és un atac —pot ser un format que no és a SIGNATURES—, però sí que és motiu per no acceptar-lo.
Solució 2. La clau és no tallar a cegues, sinó recular fins al principi d'un caràcter. En UTF-8, els bytes de continuació tenen la forma 10xxxxxx, és a dir, (byte & 0xc0) === 0x80:
function trossejarText(text, bytesMaxims) {
const complet = Buffer.from(text, 'utf8');
const trossos = [];
for (let inici = 0; inici < complet.length; ) {
let fi = Math.min(inici + bytesMaxims, complet.length);
// Reculem mentre 'fi' caigui sobre un byte de continuacio.
while (fi > inici + 1 && (complet[fi] & 0xc0) === 0x80) fi -= 1;
trossos.push(Buffer.from(complet.subarray(inici, fi)));
inici = fi;
}
return trossos;
}
console.log(trossejarText('Noche de Monólogos 🎭 en la Sala Bóveda', 10).map((t) => t.toString('utf8')));El Buffer.from(...) al voltant del subarray és imprescindible: sense ell, els trossos serien vistes del mateix buffer i qualsevol modificació posterior els afectaria tots. Una alternativa més curta si només vols text: recórrer [...text] acumulant per Buffer.byteLength de cada caràcter.
Solució 3. El format s'escriu i es llegeix en el mateix ordre en què està definit:
const SIGNATURA = Buffer.from('EVIV', 'latin1');
const VERSIO = 1;
function serialitzar(informe) {
const capcalera = Buffer.alloc(11);
SIGNATURA.copy(capcalera, 0);
capcalera.writeUInt8(VERSIO, 4);
capcalera.writeUInt16BE(informe.sessions.length, 5);
capcalera.writeUInt32BE(informe.entradesVenudes, 7);
return Buffer.concat([capcalera, Buffer.from(JSON.stringify(informe), 'utf8')]);
}
function deserialitzar(binari) {
if (!binari.subarray(0, 4).equals(SIGNATURA)) throw Object.assign(new Error('No es un informe'), { codi: 'FORMAT_NO_ACCEPTAT' });
return {
versio: binari.readUInt8(4),
sessions: binari.readUInt16BE(5),
entradesVenudes: binari.readUInt32BE(7),
detall: JSON.parse(binari.subarray(11).toString('utf8'))
};
}Amb les dades de la llavor, readUInt32BE(7) torna 1811 i readUInt16BE(5) torna 7. Fixa't en el byte de versió: és el que permetrà llegir fitxers antics quan el format canviï, i ometre'l és l'error més freqüent en dissenyar un format binari propi. writeUInt16BE amb més de 65.535 sessions llançaria ERR_OUT_OF_RANGE, un recordatori que cada camp binari té un sostre que cal triar a consciència.
Conclusió
Ja saps què hi ha dins de la caixa. Un Buffer és una Uint8Array sobre memòria aliena al munt de V8, de longitud fixa, que existeix perquè JavaScript no tenia tipus binari quan Node el va necessitar. El crees amb Buffer.from —copiant des de text, array o un altre buffer, compartint des d'un ArrayBuffer— o amb Buffer.alloc, mai amb allocUnsafe llevat que l'hagis de sobreescriure sencer, perquè el seu contingut inicial és memòria feta servir pel procés mateix.
Coneixes les codificacions i el seu repartiment de papers: utf8 per a text, latin1 per tractar bytes com a caràcters un a un, hex per depurar i hashes, base64 per ficar binari en JSON i base64url per ficar-lo en una URL. Saps llegir i escriure enters amb readUInt32BE i companyia, i que l'ordre de bytes el fixa el format, no la teva màquina. Domines les operacions i les seves trampes: subarray comparteix memòria —l'error clàssic—, concat copia, equals compara sense ambigüitat de codificació. I tens clar per què String.length, Buffer.byteLength i [...text].length donen tres números diferents, per què tallar un buffer per una posició arbitrària produeix un � irrecuperable i com ho evita el StringDecoder guardant-se els bytes incomplets entre tros i tros.
Escena Viva s'emporta dos mòduls nous: src/utils/tipus-fitxer.js, que valida el cartell d'un esdeveniment pel seu nombre màgic llegint només setze bytes amb FileHandle, i src/utils/qr-entrada.js, que codifica la càrrega útil del QR en base64url amb una signatura HMAC comparada en temps constant amb timingSafeEqual sobre resums d'igual longitud. I saps que buf.buffer no és el teu buffer, sinó el pool de 8 KB on viu: byteOffset i byteLength no són opcionals. Amb això tanques el Mòdul 3. Escena Viva ha passat de tenir les dades incrustades al codi a llegir el seu catàleg del disc de manera asíncrona, organitzar informes per mes, resoldre rutes que funcionen des de qualsevol directori i en qualsevol sistema, processar un històric de vendes amb memòria constant mitjançant canonades amb pipeline, i entendre els bytes que circulen per tot l'anterior. El projecte sap llegir i escriure; el que encara no sap és parlar amb ningú. Això comença al Mòdul 4: HTTP, i veuràs de seguida que no és cap territori nou: un servidor HTTP de Node és un EventEmitter que emet request; la petició que reps és un stream de lectura i la resposta que tornes és un stream d'escriptura; el cos JSON d'un POST arriba en trossos de Buffer que cal acumular amb cura; i servir un fitxer estàtic és exactament createReadStream més la ruta blindada de la lliçó 03-03. Tot el d'aquest mòdul torna, aquesta vegada connectat a la xarxa.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
