Cet article résume la présentation disponible ici. Le fichier démo résultant peut être téléchargé à la fin de l’article.
SingleFile, un outil d’archivage web, stocke habituellement les ressources des pages web sous forme d’URI de données. Cependant, cette approche peut être inefficace pour les ressources volumineuses. Une solution plus élégante combine la structure flexible du format ZIP avec HTML. Nous allons pousser cette idée plus loin en encapsulant cette structure entière dans un fichier PNG.
Le format ZIP fournit une structure organisée pour stocker plusieurs fichiers. Il repose sur une structure composée d’entrées de fichiers suivies d’un répertoire central. Le répertoire central agit comme une table des matières, contenant des en-têtes avec des métadonnées pour chaque entrée de fichier. Ces en-têtes incluent des informations essentielles telles que les noms de fichiers, les tailles, les sommes de contrôle et les décalages des entrées de fichiers. Ce qui rend le ZIP particulièrement polyvalent, c’est sa flexibilité dans le placement des données. Le format permet de préfixer des données avant le contenu ZIP en définissant un décalage supérieur à 0 pour la première entrée de fichier, tout en autorisant l’ajout de jusqu’à 64 Ko de données après (commentaire ZIP). Cette caractéristique le rend particulièrement adapté à la création de fichiers polyglottes.
Avec ces connaissances, nous pouvons créer une archive auto-extractible fonctionnant dans les navigateurs. La page à afficher et ses ressources sont stockées dans un fichier ZIP. En plaçant les données ZIP dans un commentaire HTML, nous obtenons une page auto-extractible qui extrait et affiche le contenu du fichier ZIP.
Structure de base de la page auto-extractible :
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Please wait...</title>
<script> Contenu de lib/zip.min.js </script>
</head>
<body>
<p>Please wait...</p>
<!-- [Données ZIP] -->
<script> Contenu de assets/main.js </script>
</body>
</html>
Le script assets/main.js
sur cette “page de démarrage” lit les données ZIP avec fetch("")
et utilise la bibliothèque JavaScript lib/zip.min.js
pour les extraire. Cependant, à cause de la politique de même origine, la récupération des données ZIP avec fetch("")
échoue lorsque la page est ouverte depuis le système de fichiers (sauf dans Firefox).
Pour contourner cette limitation, nous pouvons lire les données ZIP directement depuis le DOM. Cela nécessite de gérer attentivement l’encodage des caractères. La page de démarrage est encodée en windows-1252
, permettant une lecture avec une dégradation minimale. Quelques défis d’encodage apparaissent :
UTF-16
au lieu de windows-1252
U+0000
) est décodé en caractère de remplacement (U+FFFD
)\r
) et les retours chariot + saut de ligne (\r\n
) sont décodés en saut de ligne (\n
)Les deux premiers points peuvent être résolus en utilisant une table d’association pour convertir les caractères en windows-1252. Pour le dernier point, des “données de consolidation” dans une balise de script JSON sont ajoutées à la page de démarrage. Ces données suivent les décalages des retours chariot et des retours chariot suivis de sauts de ligne, permettant une reconstruction précise du contenu d’origine lors de l’extraction des données ZIP.
Voici la structure résultante :
<!doctype html>
<html>
<head>
<meta charset=windows-1252>
<title>Please wait...</title>
<script> Contenu de lib/zip.min.js </script>
</head>
<body>
<p>Please wait...</p>
<!-- [Données ZIP] -->
<script text=application/json>
[Données de consolidation]
</script>
<script> Contenu de assets/main.js </script>
</body>
</html>
Le format PNG contient une signature suivie de chunks, chacun ayant :
IHDR
(en-tête), IDAT
(données), IEND
(fin), tEXt
(données personnalisées)…Structure minimale d’un fichier PNG :
IHDR
(13 octets)IDAT
IEND
(12 octets)La mise en œuvre ultime combine les trois formats dans un seul fichier. La tolérance du format HTML permet cette complexité. Cependant, cela pose des défis :
IHDR
et IEND
deviennent brièvement visibles comme des nœuds texte et doivent être supprimés dès que la page est analyséedocument.write()
pour analyser la page affichéeStructure vue comme des chunks PNG :
[Signature PNG]
[Chunk IHDR]
[Chunk tEXt
<!doctype html>
<html>
<head>
<meta charset=windows-1252>
<title>Please wait...</title>
<script> Contenu de lib/zip.min.js </script>
</head>
<body>
<p>Please wait...</p>
<!--
]
[Chunks IDAT]
[Chunk tEXt
-->
<!-- [Données ZIP] -->
<script text=application/json>
[Données de consolidation]
</script>
<script> Contenu de assets/main.js </script>
</body>
</html>
]
[Chunk IEND]
Diagramme de la structure résultante:
graph TD
A[Fichier PNG] --> B[Signature PNG]
A --> C[Chunk IHDR]
A --> D[Chunk tEXt]
A --> E[Chunks IDAT]
A --> F[Chunk tEXt]
A --> G[Chunk IEND]
D --> H[Début HTML]
F --> L[Données ZIP dans un Commentaire HTML]
L --> M[Entrées de fichier]
L --> N[Annuaire Central]
F --> P[Fin HTML]
P --> Q[Scripts]
P --> R[Fin Body]
L’optimisation finale retire l’image principale (i.e. le logo de RennesJS) du fichier ZIP et réutilise la page, interprétée comme un fichier PNG, pour la remplacer dans la page affichée.
Téléchargez demo.png.zip.html (un bug dans “Archive Utility” sur macOS empêche la décompression du fichier résultant ; vous pouvez utiliser unzip
pour contourner ce problème). Vous pouvez également afficher le fichier demo.png.zip.html.