UNPKG

dnsweeper

Version:

Advanced CLI tool for DNS record risk analysis and cleanup. Features CSV import for Cloudflare/Route53, automated risk assessment, and parallel DNS validation.

249 lines 8.93 kB
/** * CSV文字エンコーディング自動検出ユーティリティ */ import { readFile } from 'node:fs/promises'; import { TextDecoder } from 'node:util'; import { detect as chardetDetect } from 'chardet'; import { DnsSweeperError } from '../lib/errors.js'; /** * バイト順マーク(BOM)の定義 */ const BOM_PATTERNS = { 'utf-8': new Uint8Array([0xef, 0xbb, 0xbf]), 'utf-16le': new Uint8Array([0xff, 0xfe]), 'utf-16be': new Uint8Array([0xfe, 0xff]), 'utf-32le': new Uint8Array([0xff, 0xfe, 0x00, 0x00]), 'utf-32be': new Uint8Array([0x00, 0x00, 0xfe, 0xff]), }; /** * chardetエンコーディング名を標準名にマッピング */ const ENCODING_MAPPING = { 'UTF-8': 'utf-8', UTF8: 'utf-8', 'UTF-16LE': 'utf-16le', 'UTF-16BE': 'utf-16be', 'UTF-16': 'utf-16le', // デフォルトとしてLE Shift_JIS: 'shift_jis', SHIFT_JIS: 'shift_jis', 'EUC-JP': 'euc-jp', 'ISO-2022-JP': 'iso-2022-jp', 'windows-1252': 'windows-1252', ASCII: 'ascii', 'US-ASCII': 'ascii', }; /** * BOMを検出 */ export function detectBOM(buffer) { const uint8Array = new Uint8Array(buffer); // UTF-32 BOMを先にチェック(UTF-16と区別するため) if (uint8Array.length >= 4) { if (uint8Array.subarray(0, 4).every((byte, i) => byte === BOM_PATTERNS['utf-32le'][i])) { return { encoding: null, bomLength: 4 }; // UTF-32はサポートしない } if (uint8Array.subarray(0, 4).every((byte, i) => byte === BOM_PATTERNS['utf-32be'][i])) { return { encoding: null, bomLength: 4 }; // UTF-32はサポートしない } } // UTF-8 BOM if (uint8Array.length >= 3 && uint8Array.subarray(0, 3).every((byte, i) => byte === BOM_PATTERNS['utf-8'][i])) { return { encoding: 'utf-8', bomLength: 3 }; } // UTF-16 BOM if (uint8Array.length >= 2) { if (uint8Array.subarray(0, 2).every((byte, i) => byte === BOM_PATTERNS['utf-16le'][i])) { return { encoding: 'utf-16le', bomLength: 2 }; } if (uint8Array.subarray(0, 2).every((byte, i) => byte === BOM_PATTERNS['utf-16be'][i])) { return { encoding: 'utf-16be', bomLength: 2 }; } } return { encoding: null, bomLength: 0 }; } /** * ファイルのエンコーディングを自動検出 */ export async function detectFileEncoding(filePath) { try { // ファイルの最初の数KBを読み込み(大容量ファイル対応) const buffer = await readFile(filePath); const sampleSize = Math.min(buffer.length, 8192); // 8KB const sampleBuffer = buffer.subarray(0, sampleSize); return detectBufferEncoding(sampleBuffer); } catch (error) { throw new DnsSweeperError(`ファイルエンコーディング検出に失敗: ${filePath}`, 'ENCODING_DETECTION_ERROR', { filePath, error: error instanceof Error ? error.message : 'Unknown error' }); } } /** * バッファのエンコーディングを自動検出 */ export function detectBufferEncoding(buffer) { // BOM検出 const bomResult = detectBOM(buffer); if (bomResult.encoding) { return { encoding: bomResult.encoding, confidence: 100, originalDetection: null, bomPresent: true, alternatives: [], }; } // chardetによる検出 const detected = chardetDetect(buffer); if (!detected) { // 検出できない場合はUTF-8をデフォルトとする return { encoding: 'utf-8', confidence: 50, originalDetection: null, bomPresent: false, alternatives: [], }; } // 検出結果の配列処理 const detections = Array.isArray(detected) ? detected : [{ name: detected, confidence: 80 }]; const primary = detections[0]; if (!primary) { return { encoding: 'utf-8', confidence: 50, originalDetection: 'utf-8', bomPresent: bomResult.bomLength > 0, alternatives: [], }; } // エンコーディング名のマッピング const mappedEncoding = ENCODING_MAPPING[primary.name.toUpperCase()] || 'utf-8'; // 代替候補の処理 const alternatives = Array.isArray(detections) ? detections .slice(1) .map((d) => ({ encoding: ENCODING_MAPPING[d.name.toUpperCase()] || 'utf-8', confidence: d.confidence || 0, })) .filter((alt) => alt.encoding !== mappedEncoding) .slice(0, 3) // 上位3つまで : []; return { encoding: mappedEncoding, confidence: primary.confidence || 80, originalDetection: primary.name, bomPresent: bomResult.bomLength > 0, alternatives, }; } /** * 指定されたエンコーディングでバッファをデコード */ export function decodeBuffer(buffer, encoding) { try { // BOMを除去 const bomResult = detectBOM(buffer); const actualBuffer = bomResult.bomLength > 0 ? buffer.subarray(bomResult.bomLength) : buffer; // TextDecoderでデコード const decoder = new TextDecoder(encoding, { fatal: false }); return decoder.decode(actualBuffer); } catch (error) { throw new DnsSweeperError(`文字デコードに失敗: ${encoding}`, 'DECODE_ERROR', { encoding, error: error instanceof Error ? error.message : 'Unknown error', }); } } /** * ファイルを適切なエンコーディングで読み込み */ export async function readFileWithDetectedEncoding(filePath) { const buffer = await readFile(filePath); const detection = detectBufferEncoding(buffer); const content = decodeBuffer(buffer, detection.encoding); return { content, detection, }; } /** * エンコーディング検出結果の信頼性を評価 */ export function evaluateDetectionReliability(result) { const recommendations = []; if (result.bomPresent) { return { level: 'high', message: 'BOMが検出されたため、エンコーディングは確実です', recommendations: [], }; } if (result.confidence >= 85) { return { level: 'high', message: `高い信頼度でエンコーディングを検出 (${result.confidence}%)`, recommendations: [], }; } if (result.confidence >= 65) { if (result.alternatives.length > 0) { recommendations.push(`代替候補: ${result.alternatives.map((alt) => `${alt.encoding} (${alt.confidence}%)`).join(', ')}`); } return { level: 'medium', message: `中程度の信頼度でエンコーディングを検出 (${result.confidence}%)`, recommendations, }; } recommendations.push('ファイルのエンコーディングを手動で指定することを検討してください'); recommendations.push('ファイルの文字化けが発生する場合は、別のエンコーディングを試してください'); if (result.alternatives.length > 0) { recommendations.push(`試行候補: ${result.alternatives.map((alt) => alt.encoding).join(', ')}`); } return { level: 'low', message: `低い信頼度でのエンコーディング検出 (${result.confidence}%)`, recommendations, }; } /** * CSVファイル用の特別な検出ロジック */ export async function detectCsvEncoding(filePath) { const buffer = await readFile(filePath); const detection = detectBufferEncoding(buffer); // CSVかどうかの判定用に最初の数行をデコード let sampleContent; try { sampleContent = decodeBuffer(buffer.subarray(0, Math.min(buffer.length, 2048)), detection.encoding); } catch { // デコードに失敗した場合はUTF-8で再試行 try { sampleContent = decodeBuffer(buffer.subarray(0, Math.min(buffer.length, 2048)), 'utf-8'); } catch { sampleContent = ''; } } const lines = sampleContent.split(/\r?\n/).slice(0, 5); // CSV判定(簡易) const looksLikeCsv = lines.length > 0 && lines.some((line) => line.includes(',') || line.includes(';') || line.includes('\t')); // 潜在的な区切り文字を検出 const delimiters = [',', ';', '\t', '|']; const potentialDelimiters = delimiters.filter((delimiter) => lines.some((line) => line.includes(delimiter))); return { ...detection, csvSpecificInfo: { looksLikeCsv, sampleLines: lines, potentialDelimiters, }, }; } //# sourceMappingURL=encoding-detector.js.map