import * as plugins from '../plugins.js'; import { BaseUpstream } from '../upstream/classes.baseupstream.js'; import type { IProtocolUpstreamConfig, IUpstreamFetchContext, IUpstreamRegistryConfig, } from '../upstream/interfaces.upstream.js'; /** * Cargo-specific upstream implementation. * * Handles: * - Crate metadata (index) fetching * - Crate file (.crate) downloading * - Sparse index protocol support * - Content-addressable caching for .crate files */ export class CargoUpstream extends BaseUpstream { protected readonly protocolName = 'cargo'; /** Base URL for crate downloads (may differ from index URL) */ private readonly downloadUrl: string; constructor( config: IProtocolUpstreamConfig, downloadUrl?: string, logger?: plugins.smartlog.Smartlog, ) { super(config, logger); // Default to crates.io download URL if not specified this.downloadUrl = downloadUrl || 'https://static.crates.io/crates'; } /** * Fetch crate metadata from the sparse index. */ public async fetchCrateIndex(crateName: string): Promise { const path = this.buildIndexPath(crateName); const context: IUpstreamFetchContext = { protocol: 'cargo', resource: crateName, resourceType: 'index', path, method: 'GET', headers: { 'accept': 'text/plain', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } if (Buffer.isBuffer(result.body)) { return result.body.toString('utf8'); } return typeof result.body === 'string' ? result.body : null; } /** * Fetch a crate file from upstream. */ public async fetchCrate(crateName: string, version: string): Promise { // Crate downloads typically go to a different URL than the index const path = `/${crateName}/${crateName}-${version}.crate`; const context: IUpstreamFetchContext = { protocol: 'cargo', resource: crateName, resourceType: 'crate', path, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; // Use special handling for crate downloads const result = await this.fetchCrateFile(crateName, version); return result; } /** * Fetch crate file directly from the download URL. */ private async fetchCrateFile(crateName: string, version: string): Promise { const context: IUpstreamFetchContext = { protocol: 'cargo', resource: crateName, resourceType: 'crate', path: `/${crateName}/${crateName}-${version}.crate`, method: 'GET', headers: { 'accept': 'application/octet-stream', }, query: {}, }; const result = await this.fetch(context); if (!result || !result.success) { return null; } return Buffer.isBuffer(result.body) ? result.body : Buffer.from(result.body); } /** * Build the sparse index path for a crate. * * Path structure: * - 1 char: /1/{name} * - 2 chars: /2/{name} * - 3 chars: /3/{first char}/{name} * - 4+ chars: /{first 2}/{next 2}/{name} */ private buildIndexPath(crateName: string): string { const lowerName = crateName.toLowerCase(); const len = lowerName.length; if (len === 1) { return `/1/${lowerName}`; } else if (len === 2) { return `/2/${lowerName}`; } else if (len === 3) { return `/3/${lowerName[0]}/${lowerName}`; } else { return `/${lowerName.slice(0, 2)}/${lowerName.slice(2, 4)}/${lowerName}`; } } /** * Override URL building for Cargo-specific handling. */ protected buildUpstreamUrl( upstream: IUpstreamRegistryConfig, context: IUpstreamFetchContext, ): string { let baseUrl = upstream.url; // For crate downloads, use the download URL if (context.resourceType === 'crate') { baseUrl = this.downloadUrl; } // Remove trailing slash if (baseUrl.endsWith('/')) { baseUrl = baseUrl.slice(0, -1); } return `${baseUrl}${context.path}`; } }