All files / src/lib/dsn scanner.ts

95.45% Statements 21/22
100% Branches 9/9
100% Functions 1/1
95.45% Lines 21/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                                                                                                                                  81x 81x 81x   81x 439x   439x       439x 407x           32x 32x 32x       32x   32x 31x 31x 30x     30x   30x 19x         32x                   62x    
/**
 * File Scanner Utilities
 *
 * Shared utilities for scanning specific files and extracting DSNs.
 * Used by env-file detection for scanning .env file variants.
 */
 
import { open } from "node:fs/promises";
import { join } from "node:path";
import { handleFileError, isRegularFile } from "./fs-utils.js";
import type { DetectedDsn } from "./types.js";
 
/** Result of processing a single file for DSN extraction. */
export type FileProcessResult = {
  /** Extracted DSN string (null if not found) */
  dsn: string | null;
  /** Additional metadata to attach to the DetectedDsn */
  metadata?: {
    packagePath?: string;
  };
};
 
/** Result of scanning specific files, including mtimes for caching. */
export type SpecificFileScanResult = {
  /** Detected DSNs */
  dsns: DetectedDsn[];
  /** Map of source file paths to their mtimes (only files containing DSNs) */
  sourceMtimes: Record<string, number>;
};
 
/**
 * Function that processes file content and extracts DSN.
 *
 * @param relativePath - Path relative to scan root
 * @param content - File content
 * @returns Extraction result or null to skip this file
 */
export type FileProcessor = (
  relativePath: string,
  content: string
) => FileProcessResult | null;
 
/**
 * Scan specific files (not glob) and extract DSNs.
 *
 * Used when scanning a known list of files (e.g., .env variants).
 *
 * @param cwd - Root directory
 * @param filenames - List of filenames to check (relative to cwd)
 * @param options - Processing options
 * @returns Object with detected DSNs and source file mtimes
 */
export async function scanSpecificFiles(
  cwd: string,
  filenames: string[],
  options: {
    stopOnFirst?: boolean;
    processFile: FileProcessor;
    createDsn: (
      raw: string,
      relativePath: string,
      metadata?: { packagePath?: string }
    ) => DetectedDsn | null;
  }
): Promise<SpecificFileScanResult> {
  const { stopOnFirst = false, processFile, createDsn } = options;
  const dsns: DetectedDsn[] = [];
  const sourceMtimes: Record<string, number> = {};
 
  for (const filename of filenames) {
    const filepath = join(cwd, filename);
 
    try {
      // Guard: skip non-regular files (FIFOs, sockets, etc.) that would block.
      // 1Password streams secrets via symlinked named pipes; open() on a FIFO
      // blocks indefinitely waiting for a writer.
      if (!(await isRegularFile(filepath, "scanSpecificFiles.stat"))) {
        continue;
      }
 
      // Use a single file handle for atomic read + stat to avoid TOCTOU:
      // reading content and mtime from the same open handle ensures they
      // correspond to the same file version.
      const fh = await open(filepath, "r");
      try {
        const [content, stats] = await Promise.all([
          fh.readFile("utf-8"),
          fh.stat(),
        ]);
        const result = processFile(filename, content);
 
        if (result?.dsn) {
          const detected = createDsn(result.dsn, filename, result.metadata);
          if (detected) {
            dsns.push(detected);
            // Record mtime for cache invalidation (from same handle as content).
            // Floor to integer — all mtime comparisons in dsn-cache.ts use Math.floor.
            sourceMtimes[filename] = Math.floor(stats.mtimeMs);
 
            if (stopOnFirst) {
              return { dsns, sourceMtimes };
            }
          }
        }
      } finally {
        await fh.close();
      }
    } catch (error) {
      handleFileError(error, {
        operation: "scanSpecificFiles",
        path: filepath,
      });
    }
  }
 
  return { dsns, sourceMtimes };
}