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
@@ -16,6 +16,7 @@ import {
swiftEqual,
swiftHasPrefix,
swiftLowercased,
swiftPathExtension,
swiftTrim,
} from '../util/swiftString.js';
import { DEFAULT_IMAGE_FILES, LEGACY_DEFAULT_IMAGE_BASE64 } from './defaultImages.js';
@@ -146,6 +147,70 @@ function keyOf(path: string): string {
return path.normalize('NFC');
}
/** 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']);
/** "image/png" → "png", "image/jpeg" → "jpg", as Java names them. */
function javaSubtype(mediaType: string): string {
const name = mediaType.slice(mediaType.indexOf('/') + 1);
return name === 'jpeg' ? 'jpg' : name;
}
/**
* A file's type as Java FILIUS keeps it (Datei.dateiTyp), from what the
* model keeps: a text file its extension when Java reads that as text, else
* "text"; an image or binary file its media type's subtype (none: empty).
* The project export writes it, and a server that names it (Gnutella's
* Content-type) says it.
*/
export function javaFileTypeName(name: string, content: Exclude<VirtualFileContent, { kind: 'directory' }>): string {
if (content.kind === 'text') {
const dot = name.lastIndexOf('.');
const extension = dot < 0 ? '' : name.slice(dot + 1).toLowerCase();
return TEXT_EXTENSIONS.has(extension) ? extension : 'text';
}
if (content.kind === 'image') return javaSubtype(content.mediaType);
return content.mediaType === undefined ? '' : javaSubtype(content.mediaType);
}
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']);
/**
* 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.
*/
/**
* A file's content as the model keeps it, from Java's type and content
* (Datei.dateiTyp, getDateiInhalt): an image or an unknown type decoded
* from Base64, a text type as text; what is no Base64 kept as text or bytes,
* with a warning. The project import reads files so, and a program that
* gets a file from another (Gnutella's download).
*/
export function fileContentOfJavaType(rawType: string, content: string, path: string, warn: (message: string) => void = () => {}): VirtualFileContent {
const type = swiftLowercased(swiftTrim(rawType));
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 };
}
export class VirtualFileSystem {
private store: Store;