/** * Maven helper utilities * Path conversion, XML generation, checksum calculation */ import * as plugins from '../plugins.js'; import type { IMavenCoordinate, IMavenMetadata, IChecksums, IMavenPom, } from './interfaces.maven.js'; /** * Convert Maven GAV coordinates to storage path * Example: com.example:my-lib:1.0.0 → com/example/my-lib/1.0.0 */ export function gavToPath( groupId: string, artifactId: string, version?: string ): string { const groupPath = groupId.replace(/\./g, '/'); if (version) { return `${groupPath}/${artifactId}/${version}`; } return `${groupPath}/${artifactId}`; } /** * Parse Maven path to GAV coordinates * Example: com/example/my-lib/1.0.0/my-lib-1.0.0.jar → {groupId, artifactId, version, ...} */ export function pathToGAV(path: string): IMavenCoordinate | null { // Remove leading slash if present const cleanPath = path.startsWith('/') ? path.substring(1) : path; // Split path into parts const parts = cleanPath.split('/'); if (parts.length < 4) { return null; // Not a valid artifact path } // Last part is filename const filename = parts[parts.length - 1]; const version = parts[parts.length - 2]; const artifactId = parts[parts.length - 3]; const groupId = parts.slice(0, -3).join('.'); // Parse filename to extract classifier and extension const parsed = parseFilename(filename, artifactId, version); if (!parsed) { return null; } return { groupId, artifactId, version, classifier: parsed.classifier, extension: parsed.extension, }; } /** * Parse Maven artifact filename * Example: my-lib-1.0.0-sources.jar → {classifier: 'sources', extension: 'jar'} * Example: my-lib-1.0.0.jar.md5 → {extension: 'md5'} */ export function parseFilename( filename: string, artifactId: string, version: string ): { classifier?: string; extension: string } | null { // Expected format: {artifactId}-{version}[-{classifier}].{extension}[.checksum] const prefix = `${artifactId}-${version}`; if (!filename.startsWith(prefix)) { return null; } let remainder = filename.substring(prefix.length); // Check if this is a checksum file (double extension like .jar.md5) const checksumExtensions = ['md5', 'sha1', 'sha256', 'sha512']; const lastDotIndex = remainder.lastIndexOf('.'); if (lastDotIndex !== -1) { const possibleChecksum = remainder.substring(lastDotIndex + 1); if (checksumExtensions.includes(possibleChecksum)) { // This is a checksum file - just return the checksum extension // The base artifact extension doesn't matter for checksum retrieval return { extension: possibleChecksum }; } } // Regular artifact file parsing const dotIndex = remainder.lastIndexOf('.'); if (dotIndex === -1) { return null; // No extension } const extension = remainder.substring(dotIndex + 1); const classifierPart = remainder.substring(0, dotIndex); if (classifierPart.length === 0) { // No classifier return { extension }; } if (classifierPart.startsWith('-')) { // Has classifier const classifier = classifierPart.substring(1); return { classifier, extension }; } return null; // Invalid format } /** * Build Maven artifact filename * Example: {artifactId: 'my-lib', version: '1.0.0', classifier: 'sources', extension: 'jar'} * → 'my-lib-1.0.0-sources.jar' */ export function buildFilename(coordinate: IMavenCoordinate): string { const { artifactId, version, classifier, extension } = coordinate; let filename = `${artifactId}-${version}`; if (classifier) { filename += `-${classifier}`; } filename += `.${extension}`; return filename; } /** * Calculate checksums for Maven artifact * Returns MD5, SHA-1, SHA-256, SHA-512 */ export async function calculateChecksums(data: Buffer): Promise { const crypto = await import('crypto'); return { md5: crypto.createHash('md5').update(data).digest('hex'), sha1: crypto.createHash('sha1').update(data).digest('hex'), sha256: crypto.createHash('sha256').update(data).digest('hex'), sha512: crypto.createHash('sha512').update(data).digest('hex'), }; } /** * Generate maven-metadata.xml from metadata object */ export function generateMetadataXml(metadata: IMavenMetadata): string { const { groupId, artifactId, versioning } = metadata; const { latest, release, versions, lastUpdated, snapshot, snapshotVersions } = versioning; let xml = '\n'; xml += '\n'; xml += ` ${escapeXml(groupId)}\n`; xml += ` ${escapeXml(artifactId)}\n`; // Add version if SNAPSHOT if (snapshot) { const snapshotVersion = versions[versions.length - 1]; // Assume last version is the SNAPSHOT xml += ` ${escapeXml(snapshotVersion)}\n`; } xml += ' \n'; if (latest) { xml += ` ${escapeXml(latest)}\n`; } if (release) { xml += ` ${escapeXml(release)}\n`; } xml += ' \n'; for (const version of versions) { xml += ` ${escapeXml(version)}\n`; } xml += ' \n'; xml += ` ${lastUpdated}\n`; // Add SNAPSHOT info if present if (snapshot) { xml += ' \n'; xml += ` ${escapeXml(snapshot.timestamp)}\n`; xml += ` ${snapshot.buildNumber}\n`; xml += ' \n'; } // Add SNAPSHOT versions if present if (snapshotVersions && snapshotVersions.length > 0) { xml += ' \n'; for (const sv of snapshotVersions) { xml += ' \n'; if (sv.classifier) { xml += ` ${escapeXml(sv.classifier)}\n`; } xml += ` ${escapeXml(sv.extension)}\n`; xml += ` ${escapeXml(sv.value)}\n`; xml += ` ${sv.updated}\n`; xml += ' \n'; } xml += ' \n'; } xml += ' \n'; xml += '\n'; return xml; } /** * Parse maven-metadata.xml to metadata object * Basic XML parsing for Maven metadata */ export function parseMetadataXml(xml: string): IMavenMetadata | null { try { // Simple regex-based parsing (for basic metadata) // In production, use a proper XML parser const groupIdMatch = xml.match(/([^<]+)<\/groupId>/); const artifactIdMatch = xml.match(/([^<]+)<\/artifactId>/); const latestMatch = xml.match(/([^<]+)<\/latest>/); const releaseMatch = xml.match(/([^<]+)<\/release>/); const lastUpdatedMatch = xml.match(/([^<]+)<\/lastUpdated>/); if (!groupIdMatch || !artifactIdMatch) { return null; } // Parse versions const versionsMatch = xml.match(/([\s\S]*?)<\/versions>/); const versions: string[] = []; if (versionsMatch) { const versionMatches = versionsMatch[1].matchAll(/([^<]+)<\/version>/g); for (const match of versionMatches) { versions.push(match[1]); } } return { groupId: groupIdMatch[1], artifactId: artifactIdMatch[1], versioning: { latest: latestMatch ? latestMatch[1] : undefined, release: releaseMatch ? releaseMatch[1] : undefined, versions, lastUpdated: lastUpdatedMatch ? lastUpdatedMatch[1] : formatMavenTimestamp(new Date()), }, }; } catch (error) { return null; } } /** * Escape XML special characters */ function escapeXml(str: string): string { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Format timestamp in Maven format: yyyyMMddHHmmss */ export function formatMavenTimestamp(date: Date): string { const year = date.getUTCFullYear(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); const hours = String(date.getUTCHours()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0'); const seconds = String(date.getUTCSeconds()).padStart(2, '0'); return `${year}${month}${day}${hours}${minutes}${seconds}`; } /** * Format SNAPSHOT timestamp: yyyyMMdd.HHmmss */ export function formatSnapshotTimestamp(date: Date): string { const year = date.getUTCFullYear(); const month = String(date.getUTCMonth() + 1).padStart(2, '0'); const day = String(date.getUTCDate()).padStart(2, '0'); const hours = String(date.getUTCHours()).padStart(2, '0'); const minutes = String(date.getUTCMinutes()).padStart(2, '0'); const seconds = String(date.getUTCSeconds()).padStart(2, '0'); return `${year}${month}${day}.${hours}${minutes}${seconds}`; } /** * Check if version is a SNAPSHOT */ export function isSnapshot(version: string): boolean { return version.endsWith('-SNAPSHOT'); } /** * Validate POM basic structure */ export function validatePom(pomXml: string): boolean { try { // Basic validation - check for required fields return ( pomXml.includes('') && pomXml.includes('') && pomXml.includes('') && pomXml.includes('') ); } catch (error) { return false; } } /** * Extract GAV from POM XML */ export function extractGAVFromPom(pomXml: string): { groupId: string; artifactId: string; version: string } | null { try { const groupIdMatch = pomXml.match(/([^<]+)<\/groupId>/); const artifactIdMatch = pomXml.match(/([^<]+)<\/artifactId>/); const versionMatch = pomXml.match(/([^<]+)<\/version>/); if (groupIdMatch && artifactIdMatch && versionMatch) { return { groupId: groupIdMatch[1], artifactId: artifactIdMatch[1], version: versionMatch[1], }; } return null; } catch (error) { return null; } }