feat(core): Gnutella search and download after Java, measured against Java

GnutellaRun adds a line of four peers: a search goes on as a query, each
peer answers with a hit for each matching file and the hits come back the
way the query came; a hit's file is fetched by HTTP on port 6346. The
file type names of the model move from the project export and import into
the core, so that the download uses them too.

Departures from Java: a peer that sent hits keeps its connection, so that
the hits of farther peers come back too (Java closes it and loses them),
and sizes are in characters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 14:59:26 +00:00
co-authored by Claude Opus 5.5
parent b4a95e6cac
commit ce5b10c1f3
13 changed files with 39934 additions and 130 deletions
+3 -21
View File
@@ -74,6 +74,7 @@
// Java FILIUS to the topology of the original up to those departures.
import {
javaFileTypeName,
accountFileField,
serverAccountsFileText,
javaBase64,
@@ -462,36 +463,17 @@ function putFile(root: FlsFileTree, path: string, type: string, content: string)
else directory.children[index] = file;
}
/** The text types Java's importer reads as text, named by a file's extension. */
const TEXT_EXTENSIONS = new Set(['txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
function fileTrees(files: VirtualFileSystem, directory: string): FlsFileTree[] {
return files.entries(directory).map((entry): FlsFileTree => {
const name = entry.path.slice(entry.path.lastIndexOf('/') + 1);
const file = (type: string | null, content: string): FlsFileTree => ({ kind: 'file', name, type, content, children: [] });
switch (entry.content.kind) {
case 'directory':
return { kind: 'directory', name, children: fileTrees(files, entry.path) };
case 'text': {
const dot = name.lastIndexOf('.');
const extension = dot < 0 ? '' : name.slice(dot + 1).toLowerCase();
return file(TEXT_EXTENSIONS.has(extension) ? extension : 'text', entry.content.text);
}
case 'image':
return file(subtype(entry.content.mediaType), javaBase64(entry.content.data));
case 'binary':
return file(entry.content.mediaType === undefined ? '' : subtype(entry.content.mediaType), javaBase64(entry.content.data));
}
if (entry.content.kind === 'directory') return { kind: 'directory', name, children: fileTrees(files, entry.path) };
return file(javaFileTypeName(name, entry.content), entry.content.kind === 'text' ? entry.content.text : javaBase64(entry.content.data));
});
}
export { javaBase64 };
/** "image/png" → "png", "image/jpeg" → "jpg", as Java names them. */
function subtype(mediaType: string): string {
const name = mediaType.slice(mediaType.indexOf('/') + 1);
return name === 'jpeg' ? 'jpg' : name;
}
function exportDhcpServer(server: DhcpServerConfiguration): FlsDhcpServer {
return {
+2 -39
View File
@@ -43,6 +43,7 @@
// "IN", names with a leading digit — and drops Java's duplicates.
import {
fileContentOfJavaType,
canonicalMacAddress,
forwardsIPv4ByDefault,
deepEqual,
@@ -662,44 +663,6 @@ function firewallConfigurationOf(firewall: FlsFirewall, where: string, warnings:
};
}
const IMAGE_TYPES = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'image']);
const TEXT_TYPES = new Set(['', 'text', 'txt', 'text/txt', 'html', 'htm', 'css', 'xml', 'csv', 'conf', 'log']);
const encoder = new TextEncoder();
/**
* Swift's importedContent: an image by type or extension (Base64), text by
* type, else Base64 as binary data, else text — each fallback with a warning.
*/
function importedContent(
rawType: string | null,
rawContent: string | null,
path: string,
warn: (message: string) => void,
): VirtualFileContent {
const type = swiftLowercased(trimmedOrEmpty(rawType));
const content = rawContent ?? '';
const extension = swiftLowercased(swiftPathExtension(path));
const binary = (data: Uint8Array): VirtualFileContent =>
type === '' ? { kind: 'binary', data } : { kind: 'binary', data, mediaType: `application/${type}` };
if (IMAGE_TYPES.has(type) || IMAGE_TYPES.has(extension)) {
const data = swiftBase64Decode(content);
if (data !== undefined) {
const subtype = type === 'image' || type === '' ? (extension === '' ? 'png' : extension) : type;
return { kind: 'image', data, mediaType: `image/${subtype === 'jpg' ? 'jpeg' : subtype}` };
}
warn(`Java file '${path}' declared image type '${type}' but was not valid Base64; imported as UTF-8 binary data.`);
return binary(encoder.encode(content));
}
if (TEXT_TYPES.has(type)) return { kind: 'text', text: content };
const data = swiftBase64Decode(content);
if (data !== undefined && (data.length > 0 || swiftTrim(content) === '')) {
warn(`Java file '${path}' used unknown type '${type}'; preserved as binary data.`);
return binary(data);
}
warn(`Java file '${path}' used unknown type '${type}'; preserved as UTF-8 text.`);
return { kind: 'text', text: content };
}
/** A usable name: not empty after trimming, no path separator. */
function safeName(name: string | null): string | undefined {
const trimmed = trimmedOrEmpty(name);
@@ -726,7 +689,7 @@ function importFileSystem(root: FlsFileTree | undefined, nodeName: string, warni
const rawPath = parent === '/' ? `/${name}` : `${parent}/${name}`;
try {
const path = normalizedAbsolutePath(rawPath);
const content = importedContent(tree.type, tree.content, path, warn);
const content = fileContentOfJavaType(trimmedOrEmpty(tree.type), tree.content ?? '', path, warn);
switch (content.kind) {
case 'text':
fileSystem.writeTextFile(path, content.text, false);