or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

async-programming.mdcharacter-operations.mdcollections.mdconfiguration.mdcore-infrastructure.mddata-encoding.mddate-time.mdexternal-integration.mdindex.mdnumeric-types.mdreactive-programming.mdstring-operations.mdtype-system.md
tile.json

data-encoding.mddocs/

Data Encoding

Text encoding, binary data conversion, and URI handling utilities providing comprehensive data transformation capabilities with .NET compatibility for encoding operations and binary serialization.

Encoding Module - Text Encoding

Text encoding and decoding utilities for converting between strings and byte arrays with UTF-8 support.

UTF-8 Encoding Functions

// Get UTF-8 encoder instance
function get_UTF8(): any;

// Convert string to UTF-8 bytes
function toBytes(str: string): Uint8Array;

// Convert UTF-8 bytes to string
function toString(bytes: Uint8Array): string;

// Usage
import { get_UTF8, toBytes, toString } from "fable-library/Encoding.js";

// Get UTF-8 encoder (for compatibility with .NET Encoding.UTF8)
const utf8Encoder = get_UTF8();

// String to bytes conversion
const text = "Hello, 世界! 🌍";
const bytes = toBytes(text);
console.log(bytes); // Uint8Array with UTF-8 encoded bytes

// Bytes to string conversion
const decoded = toString(bytes);
console.log(decoded); // "Hello, 世界! 🌍"
console.log(text === decoded); // true

// Handle different character sets
const asciiText = "Hello World";
const asciiBytes = toBytes(asciiText);
console.log(asciiBytes.length); // 11 bytes (1 byte per ASCII character)

const unicodeText = "Héllo Wörld";
const unicodeBytes = toBytes(unicodeText);
console.log(unicodeBytes.length); // 13 bytes (accented characters need 2 bytes)

const emojiText = "Hello 😀";
const emojiBytes = toBytes(emojiText);
console.log(emojiBytes.length); // 10 bytes (emoji needs 4 bytes)

Encoding Patterns

Safe Text Processing

import { toBytes, toString } from "fable-library/Encoding.js";

// Safe text roundtrip function
function safeTextRoundtrip(text: string): boolean {
    try {
        const encoded = toBytes(text);
        const decoded = toString(encoded);
        return text === decoded;
    } catch (error) {
        console.error("Encoding error:", error);
        return false;
    }
}

// Test various text types
const testTexts = [
    "ASCII text",
    "Latin characters: café, naïve",
    "Cyrillic: Привет мир",
    "Chinese: 你好世界",
    "Japanese: こんにちは世界",
    "Arabic: مرحبا بالعالم",
    "Emojis: 🎉🌟🚀💻",
    "Mixed: Hello世界🌍"
];

testTexts.forEach(text => {
    const isValid = safeTextRoundtrip(text);
    console.log(`"${text}" - Valid roundtrip: ${isValid}`);
});

Binary Data Transmission

import { toBytes, toString } from "fable-library/Encoding.js";

// Prepare text for binary transmission
function prepareForTransmission(data: any): Uint8Array {
    const jsonString = JSON.stringify(data);
    return toBytes(jsonString);
}

// Restore data from binary transmission
function restoreFromTransmission(bytes: Uint8Array): any {
    const jsonString = toString(bytes);
    return JSON.parse(jsonString);
}

// Example usage
const originalData = {
    id: 123,
    name: "José María",
    location: "São Paulo",
    tags: ["développement", "软件", "🚀"],
    timestamp: new Date().toISOString()
};

// Prepare for transmission
const transmissionBytes = prepareForTransmission(originalData);
console.log(`Data size: ${transmissionBytes.length} bytes`);

// Simulate transmission (e.g., over network, file storage)
// ... bytes would be transmitted/stored ...

// Restore data
const restoredData = restoreFromTransmission(transmissionBytes);
console.log("Original:", originalData);
console.log("Restored:", restoredData);
console.log("Equal:", JSON.stringify(originalData) === JSON.stringify(restoredData));

BitConverter Module - Binary Data Conversion

Binary data conversion utilities matching .NET BitConverter for converting between primitive types and byte arrays.

Endianness

// System endianness flag
const isLittleEndian: boolean;

// Usage
import { isLittleEndian } from "fable-library/BitConverter.js";

console.log(`System is ${isLittleEndian ? 'little' : 'big'} endian`);
// Most modern systems are little endian

Boolean Conversion

// Convert boolean to byte array
function getBytesBoolean(value: boolean): number[];

// Convert byte array to boolean
function toBoolean(bytes: ArrayLike<number>, startIndex: number): boolean;

// Usage
import { getBytesBoolean, toBoolean } from "fable-library/BitConverter.js";

// Boolean to bytes
const trueBytes = getBytesBoolean(true);   // [1]
const falseBytes = getBytesBoolean(false); // [0]

// Bytes to boolean
const value1 = toBoolean([1], 0);    // true
const value2 = toBoolean([0], 0);    // false
const value3 = toBoolean([42], 0);   // true (non-zero is true)

console.log(trueBytes);  // [1]
console.log(falseBytes); // [0]
console.log(value1, value2, value3); // true false true

Character Conversion

// Convert character to byte array
function getBytesChar(value: string): number[];

// Convert byte array to character
function toChar(bytes: ArrayLike<number>, startIndex: number): string;

// Usage
import { getBytesChar, toChar } from "fable-library/BitConverter.js";

// Character to bytes (UTF-16 encoding)
const charBytes = getBytesChar('A'); // [65, 0] (little endian)
const unicodeBytes = getBytesChar('€'); // [172, 32] (Euro symbol)

// Bytes to character
const char1 = toChar([65, 0], 0);   // 'A'
const char2 = toChar([172, 32], 0); // '€'

console.log(charBytes);    // [65, 0]
console.log(unicodeBytes); // [172, 32]
console.log(char1, char2); // A €

Integer Conversions

// 16-bit integers
function getBytesInt16(value: number): number[];
function getBytesUInt16(value: number): number[];
function toInt16(bytes: ArrayLike<number>, startIndex: number): number;
function toUInt16(bytes: ArrayLike<number>, startIndex: number): number;

// 32-bit integers
function getBytesInt32(value: number): number[];
function getBytesUInt32(value: number): number[];
function toInt32(bytes: ArrayLike<number>, startIndex: number): number;
function toUInt32(bytes: ArrayLike<number>, startIndex: number): number;

// Usage
import { 
    getBytesInt16, getBytesInt32, 
    toInt16, toInt32 
} from "fable-library/BitConverter.js";

// 16-bit conversions
const int16Value = 12345;
const int16Bytes = getBytesInt16(int16Value); // [57, 48] (little endian)
const restoredInt16 = toInt16(int16Bytes, 0); // 12345

// 32-bit conversions
const int32Value = 123456789;
const int32Bytes = getBytesInt32(int32Value); // [21, 205, 91, 7]
const restoredInt32 = toInt32(int32Bytes, 0); // 123456789

// Negative numbers
const negativeValue = -12345;
const negativeBytes = getBytesInt16(negativeValue); 
const restoredNegative = toInt16(negativeBytes, 0); // -12345

console.log(`${int16Value} -> [${int16Bytes}] -> ${restoredInt16}`);
console.log(`${int32Value} -> [${int32Bytes}] -> ${restoredInt32}`);
console.log(`${negativeValue} -> [${negativeBytes}] -> ${restoredNegative}`);

64-bit Integer Conversions

// 64-bit integers (using Long type)
function getBytesInt64(value: Long): number[];
function getBytesUInt64(value: Long): number[];
function toInt64(bytes: ArrayLike<number>, startIndex: number): Long;
function toUInt64(bytes: ArrayLike<number>, startIndex: number): Long;

// Usage
import { getBytesInt64, toInt64 } from "fable-library/BitConverter.js";
import { fromString, toString } from "fable-library/Long.js";

// 64-bit conversion
const longValue = fromString("9223372036854775807"); // Max int64
const longBytes = getBytesInt64(longValue);
const restoredLong = toInt64(longBytes, 0);

console.log(`Original: ${toString(longValue)}`);
console.log(`Bytes: [${longBytes.join(', ')}]`);
console.log(`Restored: ${toString(restoredLong)}`);
console.log(`Equal: ${toString(longValue) === toString(restoredLong)}`);

Floating Point Conversions

// Single precision (32-bit float)
function getBytesSingle(value: number): number[];
function toSingle(bytes: ArrayLike<number>, startIndex: number): number;

// Double precision (64-bit float)
function getBytesDouble(value: number): number[];
function toDouble(bytes: ArrayLike<number>, startIndex: number): number;

// Usage
import { 
    getBytesSingle, getBytesDouble, 
    toSingle, toDouble 
} from "fable-library/BitConverter.js";

// Single precision
const floatValue = 3.14159;
const floatBytes = getBytesSingle(floatValue);
const restoredFloat = toSingle(floatBytes, 0);

// Double precision  
const doubleValue = Math.PI;
const doubleBytes = getBytesDouble(doubleValue);
const restoredDouble = toDouble(doubleBytes, 0);

console.log(`Float: ${floatValue} -> [${floatBytes}] -> ${restoredFloat}`);
console.log(`Double: ${doubleValue} -> [${doubleBytes}] -> ${restoredDouble}`);

// Special values
const specialValues = [0, -0, Infinity, -Infinity, NaN];
specialValues.forEach(value => {
    const bytes = getBytesDouble(value);
    const restored = toDouble(bytes, 0);
    console.log(`${value} -> ${restored} (${Object.is(value, restored) ? 'exact' : 'different'})`);
});

String Representation

// Convert byte array to hex string representation
function toString(value: ArrayLike<number>, startIndex?: number, length?: number): string;

// Usage
import { toString, getBytesInt32, getBytesDouble } from "fable-library/BitConverter.js";

// Convert various data to hex string
const intBytes = getBytesInt32(123456789);
const floatBytes = getBytesDouble(3.14159);

console.log(`Int32 hex: ${toString(intBytes)}`);
console.log(`Double hex: ${toString(floatBytes)}`);

// Partial conversion
const largeArray = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
console.log(`Full: ${toString(largeArray)}`);
console.log(`Partial: ${toString(largeArray, 2, 4)}`); // Start at index 2, take 4 bytes

// Format with separators for readability
function toHexString(bytes: ArrayLike<number>, separator: string = "-"): string {
    const hex = toString(bytes);
    return hex.match(/.{2}/g)?.join(separator) || hex;
}

console.log(`Formatted: ${toHexString(intBytes)}`);     // "15-CD-5B-07"
console.log(`Formatted: ${toHexString(floatBytes, " ")}`); // "6F 12 83 C0 CA 21 09 40"

Binary Data Processing

Structured Data Serialization

import { 
    getBytesInt32, getBytesDouble, getBytesBoolean,
    toInt32, toDouble, toBoolean
} from "fable-library/BitConverter.js";
import { toBytes, toString } from "fable-library/Encoding.js";

// Serialize structured data to binary format
interface PersonData {
    id: number;
    age: number;
    height: number;
    isActive: boolean;
    name: string;
}

function serializePerson(person: PersonData): Uint8Array {
    const parts: number[] = [];
    
    // Serialize fields in order
    parts.push(...getBytesInt32(person.id));
    parts.push(...getBytesInt32(person.age));
    parts.push(...getBytesDouble(person.height));
    parts.push(...getBytesBoolean(person.isActive));
    
    // String needs length prefix
    const nameBytes = toBytes(person.name);
    parts.push(...getBytesInt32(nameBytes.length));
    parts.push(...Array.from(nameBytes));
    
    return new Uint8Array(parts);
}

function deserializePerson(data: Uint8Array): PersonData {
    const bytes = Array.from(data);
    let offset = 0;
    
    // Deserialize fields in same order
    const id = toInt32(bytes, offset); offset += 4;
    const age = toInt32(bytes, offset); offset += 4;
    const height = toDouble(bytes, offset); offset += 8;
    const isActive = toBoolean(bytes, offset); offset += 1;
    
    // String with length prefix
    const nameLength = toInt32(bytes, offset); offset += 4;
    const nameBytes = new Uint8Array(bytes.slice(offset, offset + nameLength));
    const name = toString(nameBytes);
    
    return { id, age, height, isActive, name };
}

// Usage
const originalPerson: PersonData = {
    id: 12345,
    age: 30,
    height: 175.5,
    isActive: true,
    name: "José María"
};

const serialized = serializePerson(originalPerson);
console.log(`Serialized size: ${serialized.length} bytes`);

const deserialized = deserializePerson(serialized);
console.log("Original:", originalPerson);
console.log("Deserialized:", deserialized);

Network Protocol Implementation

import { 
    getBytesInt16, getBytesInt32, 
    toInt16, toInt32 
} from "fable-library/BitConverter.js";

// Simple network message protocol
interface NetworkMessage {
    messageType: number;
    messageId: number;
    payload: Uint8Array;
}

function encodeNetworkMessage(message: NetworkMessage): Uint8Array {
    const header: number[] = [];
    
    // Protocol header: type(2) + id(4) + length(4)
    header.push(...getBytesInt16(message.messageType));
    header.push(...getBytesInt32(message.messageId));
    header.push(...getBytesInt32(message.payload.length));
    
    // Combine header and payload
    const result = new Uint8Array(header.length + message.payload.length);
    result.set(header, 0);
    result.set(message.payload, header.length);
    
    return result;
}

function decodeNetworkMessage(data: Uint8Array): NetworkMessage {
    const bytes = Array.from(data);
    
    // Parse header
    const messageType = toInt16(bytes, 0);
    const messageId = toInt32(bytes, 2);
    const payloadLength = toInt32(bytes, 6);
    
    // Extract payload
    const payload = data.slice(10, 10 + payloadLength);
    
    return { messageType, messageId, payload };
}

// Usage
const testMessage: NetworkMessage = {
    messageType: 100,
    messageId: 12345,
    payload: new Uint8Array([1, 2, 3, 4, 5])
};

const encoded = encodeNetworkMessage(testMessage);
console.log(`Encoded message size: ${encoded.length} bytes`);

const decoded = decodeNetworkMessage(encoded);
console.log("Original message type:", testMessage.messageType);
console.log("Decoded message type:", decoded.messageType);
console.log("Payloads equal:", 
    Array.from(testMessage.payload).join(',') === 
    Array.from(decoded.payload).join(',')
);

Uri Module - URI Handling

URI parsing, validation, and manipulation utilities with support for different URI schemes and formats.

URI Scheme Constants

// Standard URI schemes
const uriSchemeFile: string;   // "file"
const uriSchemeHttp: string;   // "http"  
const uriSchemeHttps: string;  // "https"

// Usage
import { uriSchemeFile, uriSchemeHttp, uriSchemeHttps } from "fable-library/Uri.js";

console.log("File scheme:", uriSchemeFile);   // "file"
console.log("HTTP scheme:", uriSchemeHttp);   // "http"
console.log("HTTPS scheme:", uriSchemeHttps); // "https"

// Build URIs with correct schemes
const fileUri = `${uriSchemeFile}:///C:/temp/data.txt`;
const httpUri = `${uriSchemeHttp}://example.com/api/data`;
const httpsUri = `${uriSchemeHttps}://secure.example.com/api/data`;

URI Kind Enumeration

enum UriKind {
    RelativeOrAbsolute,  // Can be either relative or absolute
    Absolute,           // Must be absolute (with scheme)
    Relative            // Must be relative (without scheme)
}

// Usage
import { UriKind } from "fable-library/Uri.js";

// Examples of different URI kinds
const absoluteUri = "https://example.com/path";        // UriKind.Absolute
const relativeUri = "/path/to/resource";               // UriKind.Relative
const ambiguousUri = "example.com/path";               // UriKind.RelativeOrAbsolute

URI Validation Functions

// Check if path is UNC (Universal Naming Convention)
function isUnc(path: string): boolean;

// Validate URI scheme name
function checkSchemeName(schemeName: string): boolean;

// Validate URI format
function isWellFormedUriString(uriString: string, uriKind: UriKind): boolean;

// Usage
import { isUnc, checkSchemeName, isWellFormedUriString, UriKind } from "fable-library/Uri.js";

// UNC path checking
console.log(isUnc("\\\\server\\share\\file.txt")); // true
console.log(isUnc("C:\\local\\file.txt"));          // false
console.log(isUnc("/unix/path"));                   // false

// Scheme validation
console.log(checkSchemeName("http"));     // true
console.log(checkSchemeName("https"));    // true
console.log(checkSchemeName("ftp"));      // true
console.log(checkSchemeName("custom+"));  // true (allows some special chars)
console.log(checkSchemeName("123invalid")); // false (can't start with digit)
console.log(checkSchemeName(""));         // false (empty)

// URI format validation
const testUris = [
    "https://example.com/path",
    "http://localhost:8080",
    "file:///C:/temp/file.txt",
    "/relative/path",
    "example.com",
    "not a uri at all",
    "ftp://user:pass@server.com:21/path"
];

testUris.forEach(uri => {
    const isAbsolute = isWellFormedUriString(uri, UriKind.Absolute);
    const isRelative = isWellFormedUriString(uri, UriKind.Relative);
    const isEither = isWellFormedUriString(uri, UriKind.RelativeOrAbsolute);
    
    console.log(`"${uri}"`);
    console.log(`  Absolute: ${isAbsolute}, Relative: ${isRelative}, Either: ${isEither}`);
});

URI Processing Examples

URI Validation and Parsing

import { checkSchemeName, isWellFormedUriString, UriKind } from "fable-library/Uri.js";

// Comprehensive URI validator
function validateAndParseUri(uriString: string): {
    isValid: boolean;
    kind: 'absolute' | 'relative' | 'invalid';
    scheme?: string;
    parts?: URL | null;
    error?: string;
} {
    try {
        // Check if it's a well-formed absolute URI
        if (isWellFormedUriString(uriString, UriKind.Absolute)) {
            const url = new URL(uriString);
            const schemeValid = checkSchemeName(url.protocol.slice(0, -1)); // Remove ':'
            
            return {
                isValid: schemeValid,
                kind: 'absolute',
                scheme: url.protocol.slice(0, -1),
                parts: url,
                error: schemeValid ? undefined : 'Invalid scheme name'
            };
        }
        
        // Check if it's a valid relative URI
        if (isWellFormedUriString(uriString, UriKind.Relative)) {
            return {
                isValid: true,
                kind: 'relative',
                parts: null
            };
        }
        
        return {
            isValid: false,
            kind: 'invalid',
            error: 'Not a well-formed URI'
        };
        
    } catch (error) {
        return {
            isValid: false,
            kind: 'invalid',
            error: error instanceof Error ? error.message : 'Unknown error'
        };
    }
}

// Test various URIs
const testUris = [
    "https://www.example.com:443/path?query=value#fragment",
    "http://localhost:8080/api/v1/users",
    "file:///C:/Windows/System32/notepad.exe",
    "ftp://anonymous@ftp.example.com/pub/",
    "/api/users/123",
    "../relative/path",
    "mailto:user@example.com",
    "tel:+1-555-123-4567",
    "invalid://bad-scheme-123/path",
    "not-a-uri-at-all"
];

testUris.forEach(uri => {
    const result = validateAndParseUri(uri);
    console.log(`\n"${uri}"`);
    console.log(`  Valid: ${result.isValid}`);
    console.log(`  Kind: ${result.kind}`);
    if (result.scheme) console.log(`  Scheme: ${result.scheme}`);
    if (result.parts) {
        console.log(`  Host: ${result.parts.hostname}`);
        console.log(`  Port: ${result.parts.port || 'default'}`);
        console.log(`  Path: ${result.parts.pathname}`);
    }
    if (result.error) console.log(`  Error: ${result.error}`);
});

URI Builder and Manipulation

import { uriSchemeHttp, uriSchemeHttps, checkSchemeName } from "fable-library/Uri.js";

// URI builder class
class UriBuilder {
    private scheme: string;
    private host: string;
    private port?: number;
    private path: string;
    private query: Map<string, string>;
    private fragment?: string;
    
    constructor(baseUri?: string) {
        this.scheme = uriSchemeHttp;
        this.host = 'localhost';
        this.path = '/';
        this.query = new Map();
        
        if (baseUri) {
            this.parseUri(baseUri);
        }
    }
    
    private parseUri(uri: string): void {
        try {
            const url = new URL(uri);
            this.scheme = url.protocol.slice(0, -1);
            this.host = url.hostname;
            if (url.port) this.port = parseInt(url.port);
            this.path = url.pathname;
            this.fragment = url.hash.slice(1) || undefined;
            
            // Parse query parameters
            url.searchParams.forEach((value, key) => {
                this.query.set(key, value);
            });
        } catch (error) {
            throw new Error(`Invalid URI: ${uri}`);
        }
    }
    
    setScheme(scheme: string): UriBuilder {
        if (!checkSchemeName(scheme)) {
            throw new Error(`Invalid scheme: ${scheme}`);
        }
        this.scheme = scheme;
        return this;
    }
    
    setHost(host: string): UriBuilder {
        this.host = host;
        return this;
    }
    
    setPort(port: number): UriBuilder {
        this.port = port;
        return this;
    }
    
    setPath(path: string): UriBuilder {
        this.path = path.startsWith('/') ? path : '/' + path;
        return this;
    }
    
    addQuery(key: string, value: string): UriBuilder {
        this.query.set(key, value);
        return this;
    }
    
    removeQuery(key: string): UriBuilder {
        this.query.delete(key);
        return this;
    }
    
    setFragment(fragment: string): UriBuilder {
        this.fragment = fragment;
        return this;
    }
    
    build(): string {
        let uri = `${this.scheme}://${this.host}`;
        
        if (this.port) {
            uri += `:${this.port}`;
        }
        
        uri += this.path;
        
        if (this.query.size > 0) {
            const queryString = Array.from(this.query.entries())
                .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
                .join('&');
            uri += `?${queryString}`;
        }
        
        if (this.fragment) {
            uri += `#${encodeURIComponent(this.fragment)}`;
        }
        
        return uri;
    }
}

// Usage
const apiUri = new UriBuilder()
    .setScheme(uriSchemeHttps)
    .setHost('api.example.com')
    .setPort(443)
    .setPath('/v2/users')
    .addQuery('page', '1')
    .addQuery('limit', '50')
    .addQuery('filter', 'active')
    .setFragment('results')
    .build();

console.log("Built URI:", apiUri);
// "https://api.example.com:443/v2/users?page=1&limit=50&filter=active#results"

// Modify existing URI
const modifiedUri = new UriBuilder(apiUri)
    .addQuery('sort', 'name')
    .removeQuery('filter')
    .setPath('/v2/users/search')
    .build();

console.log("Modified URI:", modifiedUri);
// "https://api.example.com:443/v2/users/search?page=1&limit=50&sort=name#results"

File Path and URI Conversion

import { uriSchemeFile, isUnc } from "fable-library/Uri.js";

// Convert file paths to file URIs
function pathToFileUri(filePath: string): string {
    // Handle different path formats
    if (isUnc(filePath)) {
        // UNC path: \\server\share\path -> file://server/share/path
        const uncPath = filePath.replace(/\\/g, '/').substring(2);
        return `${uriSchemeFile}://${uncPath}`;
    } else if (filePath.match(/^[A-Za-z]:/)) {
        // Windows absolute path: C:\path -> file:///C:/path
        const windowsPath = filePath.replace(/\\/g, '/');
        return `${uriSchemeFile}:///${windowsPath}`;
    } else if (filePath.startsWith('/')) {
        // Unix absolute path: /path -> file:///path
        return `${uriSchemeFile}://${filePath}`;
    } else {
        // Relative path - convert to absolute first
        throw new Error('Relative paths must be converted to absolute paths first');
    }
}

// Convert file URI back to path
function fileUriToPath(fileUri: string): string {
    if (!fileUri.startsWith(`${uriSchemeFile}:`)) {
        throw new Error('Not a file URI');
    }
    
    const url = new URL(fileUri);
    
    if (url.hostname) {
        // UNC path: file://server/share/path -> \\server\share\path
        return `\\\\${url.hostname}${url.pathname.replace(/\//g, '\\')}`;
    } else {
        // Local path
        let path = decodeURIComponent(url.pathname);
        
        if (path.match(/^\/[A-Za-z]:/)) {
            // Windows: file:///C:/path -> C:\path
            return path.substring(1).replace(/\//g, '\\');
        } else {
            // Unix: file:///path -> /path
            return path;
        }
    }
}

// Test file path conversions
const testPaths = [
    "C:\\Users\\John\\Documents\\file.txt",
    "/home/user/documents/file.txt",
    "\\\\server\\share\\documents\\file.txt"
];

testPaths.forEach(path => {
    try {
        const uri = pathToFileUri(path);
        const backToPath = fileUriToPath(uri);
        
        console.log(`Original: ${path}`);
        console.log(`URI: ${uri}`);
        console.log(`Back to path: ${backToPath}`);
        console.log(`Roundtrip OK: ${path === backToPath}\n`);
    } catch (error) {
        console.log(`Error with "${path}": ${error}\n`);
    }
});