2025-11-21 17:13:06 +00:00
|
|
|
/**
|
|
|
|
|
* Helper functions for RubyGems registry
|
|
|
|
|
* Compact Index generation, dependency formatting, etc.
|
|
|
|
|
*/
|
|
|
|
|
|
2025-11-25 15:07:59 +00:00
|
|
|
import * as plugins from '../plugins.js';
|
|
|
|
|
|
2025-11-21 17:13:06 +00:00
|
|
|
import type {
|
|
|
|
|
IRubyGemsVersion,
|
|
|
|
|
IRubyGemsDependency,
|
|
|
|
|
IRubyGemsRequirement,
|
|
|
|
|
ICompactIndexVersionsEntry,
|
|
|
|
|
ICompactIndexInfoEntry,
|
|
|
|
|
IRubyGemsMetadata,
|
|
|
|
|
} from './interfaces.rubygems.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate Compact Index versions file
|
|
|
|
|
* Format: GEMNAME [-]VERSION_PLATFORM[,VERSION_PLATFORM,...] MD5
|
|
|
|
|
* @param entries - Version entries for all gems
|
|
|
|
|
* @returns Compact Index versions file content
|
|
|
|
|
*/
|
|
|
|
|
export function generateCompactIndexVersions(entries: ICompactIndexVersionsEntry[]): string {
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
|
|
|
|
|
// Add metadata header
|
|
|
|
|
lines.push(`created_at: ${new Date().toISOString()}`);
|
|
|
|
|
lines.push('---');
|
|
|
|
|
|
|
|
|
|
// Add gem entries
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const versions = entry.versions
|
|
|
|
|
.map(v => {
|
|
|
|
|
const yanked = v.yanked ? '-' : '';
|
|
|
|
|
const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
|
|
|
|
|
return `${yanked}${v.version}${platform}`;
|
|
|
|
|
})
|
|
|
|
|
.join(',');
|
|
|
|
|
|
|
|
|
|
lines.push(`${entry.name} ${versions} ${entry.infoChecksum}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate Compact Index info file for a gem
|
|
|
|
|
* Format: VERSION[-PLATFORM] [DEP[,DEP,...]]|REQ[,REQ,...]
|
|
|
|
|
* @param entries - Info entries for gem versions
|
|
|
|
|
* @returns Compact Index info file content
|
|
|
|
|
*/
|
|
|
|
|
export function generateCompactIndexInfo(entries: ICompactIndexInfoEntry[]): string {
|
|
|
|
|
const lines: string[] = ['---']; // Info files start with ---
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
// Build version string with optional platform
|
|
|
|
|
const versionStr = entry.platform && entry.platform !== 'ruby'
|
|
|
|
|
? `${entry.version}-${entry.platform}`
|
|
|
|
|
: entry.version;
|
|
|
|
|
|
|
|
|
|
// Build dependencies string
|
|
|
|
|
const depsStr = entry.dependencies.length > 0
|
|
|
|
|
? entry.dependencies.map(formatDependency).join(',')
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
// Build requirements string (checksum is always required)
|
|
|
|
|
const reqParts: string[] = [`checksum:${entry.checksum}`];
|
|
|
|
|
|
|
|
|
|
for (const req of entry.requirements) {
|
|
|
|
|
reqParts.push(`${req.type}:${req.requirement}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const reqStr = reqParts.join(',');
|
|
|
|
|
|
|
|
|
|
// Combine: VERSION[-PLATFORM] [DEPS]|REQS
|
|
|
|
|
const depPart = depsStr ? ` ${depsStr}` : '';
|
|
|
|
|
lines.push(`${versionStr}${depPart}|${reqStr}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format a dependency for Compact Index
|
|
|
|
|
* Format: GEM:CONSTRAINT[&CONSTRAINT]
|
|
|
|
|
* @param dep - Dependency object
|
|
|
|
|
* @returns Formatted dependency string
|
|
|
|
|
*/
|
|
|
|
|
export function formatDependency(dep: IRubyGemsDependency): string {
|
|
|
|
|
return `${dep.name}:${dep.requirement}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse dependency string from Compact Index
|
|
|
|
|
* @param depStr - Dependency string
|
|
|
|
|
* @returns Dependency object
|
|
|
|
|
*/
|
|
|
|
|
export function parseDependency(depStr: string): IRubyGemsDependency {
|
|
|
|
|
const [name, ...reqParts] = depStr.split(':');
|
|
|
|
|
const requirement = reqParts.join(':'); // Handle :: in gem names
|
|
|
|
|
|
|
|
|
|
return { name, requirement };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate names file (newline-separated gem names)
|
|
|
|
|
* @param names - List of gem names
|
|
|
|
|
* @returns Names file content
|
|
|
|
|
*/
|
|
|
|
|
export function generateNamesFile(names: string[]): string {
|
|
|
|
|
return `---\n${names.sort().join('\n')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate MD5 hash for Compact Index checksum
|
|
|
|
|
* @param content - Content to hash
|
|
|
|
|
* @returns MD5 hash (hex)
|
|
|
|
|
*/
|
|
|
|
|
export async function calculateMD5(content: string): Promise<string> {
|
|
|
|
|
const crypto = await import('crypto');
|
|
|
|
|
return crypto.createHash('md5').update(content).digest('hex');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate SHA256 hash for gem files
|
|
|
|
|
* @param data - Data to hash
|
|
|
|
|
* @returns SHA256 hash (hex)
|
|
|
|
|
*/
|
|
|
|
|
export async function calculateSHA256(data: Buffer): Promise<string> {
|
|
|
|
|
const crypto = await import('crypto');
|
|
|
|
|
return crypto.createHash('sha256').update(data).digest('hex');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse gem filename to extract name, version, and platform
|
|
|
|
|
* @param filename - Gem filename (e.g., "rails-7.0.0-x86_64-linux.gem")
|
|
|
|
|
* @returns Parsed info or null
|
|
|
|
|
*/
|
|
|
|
|
export function parseGemFilename(filename: string): {
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
} | null {
|
|
|
|
|
if (!filename.endsWith('.gem')) return null;
|
|
|
|
|
|
|
|
|
|
const withoutExt = filename.slice(0, -4); // Remove .gem
|
|
|
|
|
|
|
|
|
|
// Try to match: name-version-platform
|
|
|
|
|
// Platform can contain hyphens (e.g., x86_64-linux)
|
|
|
|
|
const parts = withoutExt.split('-');
|
|
|
|
|
if (parts.length < 2) return null;
|
|
|
|
|
|
|
|
|
|
// Find version (first part that starts with a digit)
|
|
|
|
|
let versionIndex = -1;
|
|
|
|
|
for (let i = 1; i < parts.length; i++) {
|
|
|
|
|
if (/^\d/.test(parts[i])) {
|
|
|
|
|
versionIndex = i;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (versionIndex === -1) return null;
|
|
|
|
|
|
|
|
|
|
const name = parts.slice(0, versionIndex).join('-');
|
|
|
|
|
const version = parts[versionIndex];
|
|
|
|
|
const platform = versionIndex + 1 < parts.length
|
|
|
|
|
? parts.slice(versionIndex + 1).join('-')
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
version,
|
|
|
|
|
platform: platform && platform !== 'ruby' ? platform : undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate gem name
|
|
|
|
|
* Must contain only ASCII letters, numbers, _, and -
|
|
|
|
|
* @param name - Gem name
|
|
|
|
|
* @returns true if valid
|
|
|
|
|
*/
|
|
|
|
|
export function isValidGemName(name: string): boolean {
|
|
|
|
|
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate version string
|
|
|
|
|
* Basic semantic versioning check
|
|
|
|
|
* @param version - Version string
|
|
|
|
|
* @returns true if valid
|
|
|
|
|
*/
|
|
|
|
|
export function isValidVersion(version: string): boolean {
|
|
|
|
|
// Allow semver and other common Ruby version formats
|
|
|
|
|
return /^[\d.a-zA-Z_-]+$/.test(version);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build version list entry for Compact Index
|
|
|
|
|
* @param versions - Version info
|
|
|
|
|
* @returns Version list string
|
|
|
|
|
*/
|
|
|
|
|
export function buildVersionList(versions: Array<{
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
yanked: boolean;
|
|
|
|
|
}>): string {
|
|
|
|
|
return versions
|
|
|
|
|
.map(v => {
|
|
|
|
|
const yanked = v.yanked ? '-' : '';
|
|
|
|
|
const platform = v.platform && v.platform !== 'ruby' ? `_${v.platform}` : '';
|
|
|
|
|
return `${yanked}${v.version}${platform}`;
|
|
|
|
|
})
|
|
|
|
|
.join(',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse version list from Compact Index
|
|
|
|
|
* @param versionStr - Version list string
|
|
|
|
|
* @returns Parsed versions
|
|
|
|
|
*/
|
|
|
|
|
export function parseVersionList(versionStr: string): Array<{
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
yanked: boolean;
|
|
|
|
|
}> {
|
|
|
|
|
return versionStr.split(',').map(v => {
|
|
|
|
|
const yanked = v.startsWith('-');
|
|
|
|
|
const withoutYank = yanked ? v.substring(1) : v;
|
|
|
|
|
|
|
|
|
|
// Split on _ to separate version from platform
|
|
|
|
|
const [version, ...platformParts] = withoutYank.split('_');
|
|
|
|
|
const platform = platformParts.length > 0 ? platformParts.join('_') : undefined;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
version,
|
|
|
|
|
platform: platform && platform !== 'ruby' ? platform : undefined,
|
|
|
|
|
yanked,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate JSON response for /api/v1/versions/{gem}.json
|
|
|
|
|
* @param gemName - Gem name
|
|
|
|
|
* @param versions - Version list
|
|
|
|
|
* @returns JSON response object
|
|
|
|
|
*/
|
|
|
|
|
export function generateVersionsJson(
|
|
|
|
|
gemName: string,
|
|
|
|
|
versions: Array<{
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
uploadTime?: string;
|
|
|
|
|
}>
|
|
|
|
|
): any {
|
|
|
|
|
return {
|
|
|
|
|
name: gemName,
|
|
|
|
|
versions: versions.map(v => ({
|
|
|
|
|
number: v.version,
|
|
|
|
|
platform: v.platform || 'ruby',
|
|
|
|
|
built_at: v.uploadTime,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate JSON response for /api/v1/dependencies
|
|
|
|
|
* @param gems - Map of gem names to version dependencies
|
|
|
|
|
* @returns JSON response array
|
|
|
|
|
*/
|
|
|
|
|
export function generateDependenciesJson(gems: Map<string, Array<{
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
dependencies: IRubyGemsDependency[];
|
|
|
|
|
}>>): any {
|
|
|
|
|
const result: any[] = [];
|
|
|
|
|
|
|
|
|
|
for (const [name, versions] of gems) {
|
|
|
|
|
for (const v of versions) {
|
|
|
|
|
result.push({
|
|
|
|
|
name,
|
|
|
|
|
number: v.version,
|
|
|
|
|
platform: v.platform || 'ruby',
|
|
|
|
|
dependencies: v.dependencies.map(d => ({
|
|
|
|
|
name: d.name,
|
|
|
|
|
requirements: d.requirement,
|
|
|
|
|
})),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update Compact Index versions file with new gem version
|
|
|
|
|
* Handles append-only semantics for the current month
|
|
|
|
|
* @param existingContent - Current versions file content
|
|
|
|
|
* @param gemName - Gem name
|
|
|
|
|
* @param newVersion - New version info
|
|
|
|
|
* @param infoChecksum - MD5 of info file
|
|
|
|
|
* @returns Updated versions file content
|
|
|
|
|
*/
|
|
|
|
|
export function updateCompactIndexVersions(
|
|
|
|
|
existingContent: string,
|
|
|
|
|
gemName: string,
|
|
|
|
|
newVersion: { version: string; platform?: string; yanked: boolean },
|
|
|
|
|
infoChecksum: string
|
|
|
|
|
): string {
|
|
|
|
|
const lines = existingContent.split('\n');
|
|
|
|
|
const headerEndIndex = lines.findIndex(l => l === '---');
|
|
|
|
|
|
|
|
|
|
if (headerEndIndex === -1) {
|
|
|
|
|
throw new Error('Invalid Compact Index versions file');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const header = lines.slice(0, headerEndIndex + 1);
|
|
|
|
|
const entries = lines.slice(headerEndIndex + 1).filter(l => l.trim());
|
|
|
|
|
|
|
|
|
|
// Find existing entry for gem
|
|
|
|
|
const gemLineIndex = entries.findIndex(l => l.startsWith(`${gemName} `));
|
|
|
|
|
|
|
|
|
|
const versionStr = buildVersionList([newVersion]);
|
|
|
|
|
|
|
|
|
|
if (gemLineIndex >= 0) {
|
|
|
|
|
// Append to existing entry
|
|
|
|
|
const parts = entries[gemLineIndex].split(' ');
|
|
|
|
|
const existingVersions = parts[1];
|
|
|
|
|
const updatedVersions = `${existingVersions},${versionStr}`;
|
|
|
|
|
entries[gemLineIndex] = `${gemName} ${updatedVersions} ${infoChecksum}`;
|
|
|
|
|
} else {
|
|
|
|
|
// Add new entry
|
|
|
|
|
entries.push(`${gemName} ${versionStr} ${infoChecksum}`);
|
|
|
|
|
entries.sort(); // Keep alphabetical
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...header, ...entries].join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update Compact Index info file with new version
|
|
|
|
|
* @param existingContent - Current info file content
|
|
|
|
|
* @param newEntry - New version entry
|
|
|
|
|
* @returns Updated info file content
|
|
|
|
|
*/
|
|
|
|
|
export function updateCompactIndexInfo(
|
|
|
|
|
existingContent: string,
|
|
|
|
|
newEntry: ICompactIndexInfoEntry
|
|
|
|
|
): string {
|
|
|
|
|
const lines = existingContent ? existingContent.split('\n').filter(l => l !== '---') : [];
|
|
|
|
|
|
|
|
|
|
// Build version string
|
|
|
|
|
const versionStr = newEntry.platform && newEntry.platform !== 'ruby'
|
|
|
|
|
? `${newEntry.version}-${newEntry.platform}`
|
|
|
|
|
: newEntry.version;
|
|
|
|
|
|
|
|
|
|
// Build dependencies string
|
|
|
|
|
const depsStr = newEntry.dependencies.length > 0
|
|
|
|
|
? newEntry.dependencies.map(formatDependency).join(',')
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
// Build requirements string
|
|
|
|
|
const reqParts: string[] = [`checksum:${newEntry.checksum}`];
|
|
|
|
|
for (const req of newEntry.requirements) {
|
|
|
|
|
reqParts.push(`${req.type}:${req.requirement}`);
|
|
|
|
|
}
|
|
|
|
|
const reqStr = reqParts.join(',');
|
|
|
|
|
|
|
|
|
|
// Combine
|
|
|
|
|
const depPart = depsStr ? ` ${depsStr}` : '';
|
|
|
|
|
const newLine = `${versionStr}${depPart}|${reqStr}`;
|
|
|
|
|
|
|
|
|
|
lines.push(newLine);
|
|
|
|
|
|
|
|
|
|
return `---\n${lines.join('\n')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract gem specification from .gem file
|
|
|
|
|
* Note: This is a simplified version. Full implementation would use tar + gzip + Marshal
|
|
|
|
|
* @param gemData - Gem file data
|
|
|
|
|
* @returns Extracted spec or null
|
|
|
|
|
*/
|
|
|
|
|
export async function extractGemSpec(gemData: Buffer): Promise<any | null> {
|
|
|
|
|
try {
|
|
|
|
|
// .gem files are gzipped tar archives
|
|
|
|
|
// They contain metadata.gz which has Marshal-encoded spec
|
|
|
|
|
// This is a placeholder - full implementation would need:
|
|
|
|
|
// 1. Unzip outer gzip
|
|
|
|
|
// 2. Untar to find metadata.gz
|
|
|
|
|
// 3. Unzip metadata.gz
|
|
|
|
|
// 4. Parse Ruby Marshal format
|
|
|
|
|
|
|
|
|
|
// For now, return null and expect metadata to be provided
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-25 14:28:19 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract basic metadata from a gem file
|
2025-11-25 15:07:59 +00:00
|
|
|
* Gem files are plain tar archives (NOT gzipped) containing:
|
|
|
|
|
* - metadata.gz: gzipped YAML with gem specification
|
|
|
|
|
* - data.tar.gz: gzipped tar with actual gem files
|
|
|
|
|
* This function extracts and parses the metadata.gz to get name/version/platform
|
2025-11-25 14:28:19 +00:00
|
|
|
* @param gemData - Gem file data
|
|
|
|
|
* @returns Extracted metadata or null
|
|
|
|
|
*/
|
|
|
|
|
export async function extractGemMetadata(gemData: Buffer): Promise<{
|
|
|
|
|
name: string;
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
} | null> {
|
|
|
|
|
try {
|
2025-11-25 15:07:59 +00:00
|
|
|
// Step 1: Extract the plain tar archive to get metadata.gz
|
|
|
|
|
const smartArchive = plugins.smartarchive.SmartArchive.create();
|
|
|
|
|
const files = await smartArchive.buffer(gemData).toSmartFiles();
|
|
|
|
|
|
|
|
|
|
// Find metadata.gz
|
|
|
|
|
const metadataFile = files.find(f => f.path === 'metadata.gz' || f.relative === 'metadata.gz');
|
|
|
|
|
if (!metadataFile) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2025-11-25 14:28:19 +00:00
|
|
|
|
2025-11-25 15:07:59 +00:00
|
|
|
// Step 2: Decompress the gzipped metadata
|
|
|
|
|
const gzipTools = new plugins.smartarchive.GzipTools();
|
|
|
|
|
const metadataYaml = await gzipTools.decompress(metadataFile.contentBuffer);
|
|
|
|
|
const yamlContent = metadataYaml.toString('utf-8');
|
2025-11-25 14:28:19 +00:00
|
|
|
|
2025-11-25 15:07:59 +00:00
|
|
|
// Step 3: Parse the YAML to extract name, version, platform
|
2025-11-25 14:28:19 +00:00
|
|
|
// Look for name: field in YAML
|
2025-11-25 15:07:59 +00:00
|
|
|
const nameMatch = yamlContent.match(/name:\s*([^\n\r]+)/);
|
2025-11-25 14:28:19 +00:00
|
|
|
|
|
|
|
|
// Look for version in Ruby YAML format: version: !ruby/object:Gem::Version\n version: X.X.X
|
2025-11-25 15:07:59 +00:00
|
|
|
const versionMatch = yamlContent.match(/version:\s*!ruby\/object:Gem::Version[\s\S]*?version:\s*['"]?([^'"\n\r]+)/);
|
2025-11-25 14:28:19 +00:00
|
|
|
|
|
|
|
|
// Also try simpler version format
|
2025-11-25 15:07:59 +00:00
|
|
|
const simpleVersionMatch = !versionMatch ? yamlContent.match(/^version:\s*['"]?(\d[^'"\n\r]*)/m) : null;
|
2025-11-25 14:28:19 +00:00
|
|
|
|
|
|
|
|
// Look for platform
|
2025-11-25 15:07:59 +00:00
|
|
|
const platformMatch = yamlContent.match(/platform:\s*([^\n\r]+)/);
|
2025-11-25 14:28:19 +00:00
|
|
|
|
|
|
|
|
const name = nameMatch?.[1]?.trim();
|
|
|
|
|
const version = versionMatch?.[1]?.trim() || simpleVersionMatch?.[1]?.trim();
|
|
|
|
|
const platform = platformMatch?.[1]?.trim();
|
|
|
|
|
|
|
|
|
|
if (name && version) {
|
|
|
|
|
return {
|
|
|
|
|
name,
|
|
|
|
|
version,
|
|
|
|
|
platform: platform && platform !== 'ruby' ? platform : undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
2025-11-25 23:25:26 +00:00
|
|
|
} catch (_error) {
|
|
|
|
|
// Error handled gracefully - return null and let caller handle missing metadata
|
2025-11-25 14:28:19 +00:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-25 15:07:59 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate gzipped specs array for /specs.4.8.gz and /latest_specs.4.8.gz
|
|
|
|
|
* The format is a gzipped Ruby Marshal array of [name, version, platform] tuples
|
|
|
|
|
* Since we can't easily generate Ruby Marshal format, we'll use a simple format
|
|
|
|
|
* that represents the same data structure as a gzipped binary blob
|
|
|
|
|
* @param specs - Array of [name, version, platform] tuples
|
|
|
|
|
* @returns Gzipped specs data
|
|
|
|
|
*/
|
|
|
|
|
export async function generateSpecsGz(specs: Array<[string, string, string]>): Promise<Buffer> {
|
|
|
|
|
const gzipTools = new plugins.smartarchive.GzipTools();
|
|
|
|
|
|
|
|
|
|
// Create a simplified binary representation
|
|
|
|
|
// Real RubyGems uses Ruby Marshal format, but for compatibility we'll create
|
|
|
|
|
// a gzipped representation that tools can recognize as valid
|
|
|
|
|
|
|
|
|
|
// Format: Simple binary encoding of specs array
|
|
|
|
|
// Each spec: name_length(2 bytes) + name + version_length(2 bytes) + version + platform_length(2 bytes) + platform
|
|
|
|
|
const parts: Buffer[] = [];
|
|
|
|
|
|
|
|
|
|
// Header: number of specs (4 bytes)
|
|
|
|
|
const headerBuf = Buffer.alloc(4);
|
|
|
|
|
headerBuf.writeUInt32LE(specs.length, 0);
|
|
|
|
|
parts.push(headerBuf);
|
|
|
|
|
|
|
|
|
|
for (const [name, version, platform] of specs) {
|
|
|
|
|
const nameBuf = Buffer.from(name, 'utf-8');
|
|
|
|
|
const versionBuf = Buffer.from(version, 'utf-8');
|
|
|
|
|
const platformBuf = Buffer.from(platform, 'utf-8');
|
|
|
|
|
|
|
|
|
|
const nameLenBuf = Buffer.alloc(2);
|
|
|
|
|
nameLenBuf.writeUInt16LE(nameBuf.length, 0);
|
|
|
|
|
|
|
|
|
|
const versionLenBuf = Buffer.alloc(2);
|
|
|
|
|
versionLenBuf.writeUInt16LE(versionBuf.length, 0);
|
|
|
|
|
|
|
|
|
|
const platformLenBuf = Buffer.alloc(2);
|
|
|
|
|
platformLenBuf.writeUInt16LE(platformBuf.length, 0);
|
|
|
|
|
|
|
|
|
|
parts.push(nameLenBuf, nameBuf, versionLenBuf, versionBuf, platformLenBuf, platformBuf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uncompressed = Buffer.concat(parts);
|
|
|
|
|
return gzipTools.compress(uncompressed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate compressed gemspec for /quick/Marshal.4.8/{gem}-{version}.gemspec.rz
|
|
|
|
|
* The format is a zlib-compressed Ruby Marshal representation of the gemspec
|
|
|
|
|
* Since we can't easily generate Ruby Marshal, we'll create a simplified format
|
|
|
|
|
* @param name - Gem name
|
|
|
|
|
* @param versionMeta - Version metadata
|
|
|
|
|
* @returns Zlib-compressed gemspec data
|
|
|
|
|
*/
|
|
|
|
|
export async function generateGemspecRz(
|
|
|
|
|
name: string,
|
|
|
|
|
versionMeta: {
|
|
|
|
|
version: string;
|
|
|
|
|
platform?: string;
|
|
|
|
|
checksum: string;
|
|
|
|
|
dependencies?: Array<{ name: string; requirement: string }>;
|
|
|
|
|
}
|
|
|
|
|
): Promise<Buffer> {
|
|
|
|
|
const zlib = await import('zlib');
|
|
|
|
|
const { promisify } = await import('util');
|
|
|
|
|
const deflate = promisify(zlib.deflate);
|
|
|
|
|
|
|
|
|
|
// Create a YAML-like representation that can be parsed
|
|
|
|
|
const gemspecYaml = `--- !ruby/object:Gem::Specification
|
|
|
|
|
name: ${name}
|
|
|
|
|
version: !ruby/object:Gem::Version
|
|
|
|
|
version: ${versionMeta.version}
|
|
|
|
|
platform: ${versionMeta.platform || 'ruby'}
|
|
|
|
|
authors: []
|
|
|
|
|
date: ${new Date().toISOString().split('T')[0]}
|
|
|
|
|
dependencies: []
|
|
|
|
|
description:
|
|
|
|
|
email:
|
|
|
|
|
executables: []
|
|
|
|
|
extensions: []
|
|
|
|
|
extra_rdoc_files: []
|
|
|
|
|
files: []
|
|
|
|
|
homepage:
|
|
|
|
|
licenses: []
|
|
|
|
|
metadata: {}
|
|
|
|
|
post_install_message:
|
|
|
|
|
rdoc_options: []
|
|
|
|
|
require_paths:
|
|
|
|
|
- lib
|
|
|
|
|
required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
|
|
requirements:
|
|
|
|
|
- - ">="
|
|
|
|
|
- !ruby/object:Gem::Version
|
|
|
|
|
version: '0'
|
|
|
|
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
|
|
requirements:
|
|
|
|
|
- - ">="
|
|
|
|
|
- !ruby/object:Gem::Version
|
|
|
|
|
version: '0'
|
|
|
|
|
requirements: []
|
|
|
|
|
rubygems_version: 3.0.0
|
|
|
|
|
signing_key:
|
|
|
|
|
specification_version: 4
|
|
|
|
|
summary:
|
|
|
|
|
test_files: []
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Use zlib deflate (not gzip) for .rz files
|
|
|
|
|
return deflate(Buffer.from(gemspecYaml, 'utf-8'));
|
|
|
|
|
}
|