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
JavaScript
/**
* 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