chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
391 lines (339 loc) • 15 kB
JavaScript
/**
* ファイル名バリデータークラス
* Chrome拡張機能のファイル名の妥当性をチェックし、問題を検出する
*/
const path = require('path');
class FileNameValidator {
constructor(options = {}) {
// 設定のデフォルト値
this.options = {
allowSpaces: options.allowSpaces || false,
allowSpecialChars: options.allowSpecialChars || false,
checkPlatformCompatibility: options.checkPlatformCompatibility !== false,
autoFixSuggestions: options.autoFixSuggestions !== false,
...options
};
// ExcludeManagerを使用
this.excludeManager = options.excludeManager || null;
// プラットフォーム別の制限
this.platformLimits = {
windows: {
maxPathLength: 260,
maxFileNameLength: 255,
reservedNames: [
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
],
invalidChars: ['<', '>', ':', '"', '|', '?', '*', '\0']
},
mac: {
maxPathLength: 1024,
maxFileNameLength: 255,
invalidChars: [':', '\0']
},
linux: {
maxPathLength: 4096,
maxFileNameLength: 255,
invalidChars: ['\0']
}
};
// 問題のあるパターン
this.problematicPatterns = {
spaces: /\s/,
specialChars: /[!@#$%^&*()+=\[\]{};':"\\|,<>?]/, // 拡張子のドットを除外
nonAscii: /[^\x00-\x7F]/,
leadingDot: /^\./,
trailingDot: /\.$/,
consecutiveDots: /\.\./,
leadingHyphen: /^-/,
trailingSpace: /\s$/,
leadingSpace: /^\s/
};
// Chrome拡張機能で一般的に安全な文字セット
this.safeCharSet = /^[a-zA-Z0-9._\-]+$/;
// 検証結果
this.results = {
errors: [],
warnings: [],
suggestions: []
};
}
/**
* ファイル名またはパスを検証
*/
validate(filePath, extensionPath = '') {
this.results = {
errors: [],
warnings: [],
suggestions: []
};
const fullPath = path.join(extensionPath, filePath);
const fileName = path.basename(filePath);
const dirName = path.dirname(filePath);
// 基本的な検証
this.validateFileName(fileName, filePath);
this.validatePath(fullPath, filePath);
// プラットフォーム互換性のチェック
if (this.options.checkPlatformCompatibility) {
this.checkPlatformCompatibility(fullPath, fileName, filePath);
}
// 自動修正の提案
if (this.options.autoFixSuggestions && (this.results.errors.length > 0 || this.results.warnings.length > 0)) {
this.generateFixSuggestions(fileName, filePath);
}
return this.results;
}
/**
* ファイル名の検証
*/
validateFileName(fileName, originalPath) {
// 空のファイル名
if (!fileName || fileName.trim() === '') {
this.results.errors.push({
type: 'empty-filename',
severity: 'critical',
message: 'Empty filename',
file: originalPath
});
return;
}
// スペースのチェック
if (this.problematicPatterns.spaces.test(fileName) && !this.options.allowSpaces) {
this.results.warnings.push({
type: 'spaces-in-filename',
severity: 'high',
message: `Filename contains spaces: "${fileName}"`,
file: originalPath,
details: 'Spaces in filenames can cause issues with URLs and some build tools'
});
}
// 特殊文字のチェック
if (this.problematicPatterns.specialChars.test(fileName) && !this.options.allowSpecialChars) {
const specialChars = fileName.match(this.problematicPatterns.specialChars);
this.results.errors.push({
type: 'special-characters',
severity: 'high',
message: `Filename contains special characters: ${specialChars.join(', ')}`,
file: originalPath,
details: 'Special characters can cause issues with URLs, build tools, and cross-platform compatibility'
});
}
// 非ASCII文字のチェック
if (this.problematicPatterns.nonAscii.test(fileName)) {
this.results.warnings.push({
type: 'non-ascii-characters',
severity: 'medium',
message: `Filename contains non-ASCII characters: "${fileName}"`,
file: originalPath,
details: 'Non-ASCII characters may cause encoding issues'
});
}
// セーフな文字セットのチェック
if (!this.safeCharSet.test(fileName)) {
this.results.warnings.push({
type: 'unsafe-characters',
severity: 'medium',
message: `Filename contains characters outside safe set: "${fileName}"`,
file: originalPath,
details: 'Recommended to use only alphanumeric characters, dots, hyphens, and underscores'
});
}
// 先頭・末尾の問題
if (this.problematicPatterns.leadingDot.test(fileName)) {
this.results.warnings.push({
type: 'leading-dot',
severity: 'low',
message: `Hidden file detected: "${fileName}"`,
file: originalPath
});
}
if (this.problematicPatterns.trailingDot.test(fileName)) {
this.results.errors.push({
type: 'trailing-dot',
severity: 'high',
message: `Filename ends with dot: "${fileName}"`,
file: originalPath,
details: 'Windows strips trailing dots from filenames'
});
}
if (this.problematicPatterns.leadingSpace.test(fileName) || this.problematicPatterns.trailingSpace.test(fileName)) {
this.results.errors.push({
type: 'whitespace-edges',
severity: 'high',
message: `Filename has leading or trailing spaces: "${fileName}"`,
file: originalPath
});
}
}
/**
* パスの検証
*/
validatePath(fullPath, originalPath) {
// パスの深さをチェック
const pathDepth = fullPath.split(path.sep).filter(p => p).length;
if (pathDepth > 8) {
this.results.warnings.push({
type: 'deep-nesting',
severity: 'medium',
message: `Path is deeply nested (${pathDepth} levels)`,
file: originalPath,
details: 'Deep nesting can make the project harder to maintain'
});
}
// 連続するドット(親ディレクトリ参照)
if (this.problematicPatterns.consecutiveDots.test(fullPath)) {
this.results.errors.push({
type: 'parent-directory-reference',
severity: 'critical',
message: 'Path contains parent directory reference (..)',
file: originalPath,
details: 'Parent directory references can be a security risk'
});
}
}
/**
* プラットフォーム互換性のチェック
*/
checkPlatformCompatibility(fullPath, fileName, originalPath) {
const issues = [];
// 各プラットフォームでチェック
for (const [platform, limits] of Object.entries(this.platformLimits)) {
// パス長のチェック
if (fullPath.length > limits.maxPathLength) {
issues.push({
platform,
issue: `Path too long (${fullPath.length} > ${limits.maxPathLength})`
});
}
// ファイル名長のチェック
if (fileName.length > limits.maxFileNameLength) {
issues.push({
platform,
issue: `Filename too long (${fileName.length} > ${limits.maxFileNameLength})`
});
}
// 予約語のチェック(Windowsのみ)
if (limits.reservedNames) {
const baseName = fileName.split('.')[0].toUpperCase();
if (limits.reservedNames.includes(baseName)) {
issues.push({
platform,
issue: `Reserved filename: ${baseName}`
});
}
}
// 無効な文字のチェック
for (const char of limits.invalidChars) {
if (fileName.includes(char) || (char !== '/' && fullPath.includes(char))) {
issues.push({
platform,
issue: `Invalid character: "${char}"`
});
}
}
}
// 問題があればエラーまたは警告を追加
if (issues.length > 0) {
const platforms = [...new Set(issues.map(i => i.platform))];
const severity = platforms.includes('windows') ? 'high' : 'medium';
this.results.errors.push({
type: 'platform-compatibility',
severity,
message: `File incompatible with ${platforms.join(', ')}`,
file: originalPath,
details: issues.map(i => `${i.platform}: ${i.issue}`).join('; ')
});
}
}
/**
* 修正案の生成
*/
generateFixSuggestions(fileName, originalPath) {
let suggestedName = fileName;
// スペースをアンダースコアに置換
if (this.problematicPatterns.spaces.test(suggestedName)) {
suggestedName = suggestedName.replace(/\s+/g, '_');
}
// 特殊文字を削除またはアンダースコアに置換
if (this.problematicPatterns.specialChars.test(suggestedName)) {
suggestedName = suggestedName.replace(/[!@#$%^&*()+=\[\]{};':"\\|,<>?]/g, '_');
}
// 先頭・末尾の空白を削除
suggestedName = suggestedName.trim();
// 末尾のドットを削除
suggestedName = suggestedName.replace(/\.+$/, '');
// 連続するアンダースコアを単一に
suggestedName = suggestedName.replace(/_+/g, '_');
// 非ASCII文字をローマ字化または削除(簡易的な処理)
if (this.problematicPatterns.nonAscii.test(suggestedName)) {
suggestedName = suggestedName.replace(/[^\x00-\x7F]/g, '');
}
// 変更があれば提案を追加
if (suggestedName !== fileName && suggestedName.trim() !== '') {
this.results.suggestions.push({
type: 'rename-file',
original: originalPath,
suggested: path.join(path.dirname(originalPath), suggestedName),
reason: 'Fix filename compatibility issues'
});
}
}
/**
* ディレクトリ内のすべてのファイルを検証
*/
async validateDirectory(dirPath, extensionPath = '') {
const fs = require('fs').promises;
const allResults = {
totalFiles: 0,
problematicFiles: 0,
errors: [],
warnings: [],
suggestions: []
};
const excludeManager = this.excludeManager;
const options = this.options;
async function scanDir(currentPath, basePath) {
const entries = await fs.readdir(currentPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
const relativePath = path.relative(basePath, fullPath);
// ExcludeManagerがある場合は除外チェック
if (excludeManager) {
if (entry.isDirectory() && excludeManager.shouldExcludeDirectory(fullPath)) {
continue;
}
if (!entry.isDirectory() && excludeManager.shouldExclude(fullPath)) {
continue;
}
}
if (entry.isDirectory()) {
// ディレクトリ名も検証
const validator = new FileNameValidator(options);
const dirResults = validator.validate(relativePath, '');
allResults.errors.push(...dirResults.errors);
allResults.warnings.push(...dirResults.warnings);
allResults.suggestions.push(...dirResults.suggestions);
if (dirResults.errors.length > 0 || dirResults.warnings.length > 0) {
allResults.problematicFiles++;
}
// 再帰的にスキャン
await scanDir.call(this, fullPath, basePath);
} else if (entry.isFile()) {
allResults.totalFiles++;
const validator = new FileNameValidator(options);
const results = validator.validate(relativePath, '');
allResults.errors.push(...results.errors);
allResults.warnings.push(...results.warnings);
allResults.suggestions.push(...results.suggestions);
if (results.errors.length > 0 || results.warnings.length > 0) {
allResults.problematicFiles++;
}
}
}
}
await scanDir.call(this, dirPath, extensionPath || dirPath);
return allResults;
}
}
module.exports = FileNameValidator;