/* Copyright 2015 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 'use strict'; /* Utilities for parsing PDF.js UMD file format. A UMD header of the file * shall conform the following rules: * 1. Names of AMD modules and JavaScript object placed to the global object * shall be alike: symbols'/' and '_' removed, and character case is * ignored. * 2. CommonJS require shall use relative path to the required module, e.g. * './display.js' or '../shared/util.js', and they shall construct the * similar name to AMD one. * 3. Factory function shall contain names for modules, not less than listed * in AMD, CommonJS or global object properties list, and also their * names must be alike to name of the root object properties. * * Example: * * (function (root, factory) { * if (typeof define === 'function' && define.amd) { * define('pdfjs/display/pattern_helper', ['exports', 'pdfjs/shared/util', * 'pdfjs/display/webgl'], factory); * } else if (typeof exports !== 'undefined') { * factory(exports, require('../shared/util.js'), require('./webgl.js')); * } else { * factory((root.pdfjsDisplayPatternHelper = {}), root.pdfjsSharedUtil, * root.pdfjsDisplayWebGL); * } * }(this, function (exports, sharedUtil, displayWebGL) { * */ var fs = require('fs'); var path = require('path'); /** * Parses PDF.js UMD header. * @param {string} filePath PDF.js JavaScript file path. * @returns {{amdId: *, amdImports: Array, cjsRequires: Array, jsRootName: *, * jsImports: Array, imports: Array, importedNames: Array, * exportedNames: Array, body: *}} */ function parseUmd(filePath) { var jscode = fs.readFileSync(filePath).toString(); if (/\/\*\s*umdutils\s+ignore\s*\*\//.test(jscode)) { throw new Error('UMD processing ignored'); } // Extracts header and body. var umdStart = '\\(function\\s\\(root,\\sfactory\\)\\s\\{'; var umdImports = '\\}\\(this,\\sfunction\\s\\(exports\\b'; var umdBody = '\\)\\s\\{'; var umdEnd = '\\}\\)\\);\\s*(//#endif\\s*)?$'; var m, re; m = new RegExp(umdStart + '([\\s\\S]*?)' + umdImports + '([\\s\\S]*?)' + umdBody + '([\\s\\S]*?)' + umdEnd).exec(jscode); if (!m) { throw new Error('UMD was not found'); } var header = m[1]; var imports = m[2].replace(/\s+/g, '').split(','); imports.shift(); // avoiding only-export case var body = m[3]; // Extracts AMD definitions. var amdMatch = /\bdefine\('([^']*)',\s\[([^\]]*)\],\s+factory\);/. exec(header); if (!amdMatch) { throw new Error('AMD call was not found'); } var amdId = amdMatch[1]; var amdImports = amdMatch[2].replace(/[\s']+/g, '').split(','); if (amdImports[0] !== 'exports') { throw new Error('exports expected first at AMD call'); } amdImports.shift(); // Extracts CommonJS definitions. var cjsMatch = /\bfactory\(exports((?:,\s+require\([^\)]+\))*)\);/. exec(header); if (!cjsMatch) { throw new Error('CommonJS call was not found'); } var cjsRequires = cjsMatch[1].replace(/\s+/g, ' ').trim(). replace(/\s*require\('([^']*)'\)/g, '$1').split(','); cjsRequires.shift(); var jsMatch = /\bfactory\(\(root\.(\S+)\s=\s\{\}\)((?:,\s+root\.\S+)*)\);/. exec(header); if (!jsMatch) { throw new Error('Regular JS call was not found'); } // Extracts global object properties definitions. var jsRootName = jsMatch[1]; var jsImports = jsMatch[2].replace(/\s+/g, '').split(','); jsImports.shift(); // Scans for imports usages in the body. var importedNames = []; if (imports.length > 0) { re = new RegExp('\\b(' + imports.join('|') + ')\\.(\\w+)', 'g'); while ((m = re.exec(body))) { importedNames.push(m[0]); } } importedNames.sort(); for (var i = importedNames.length - 1; i > 0; i--) { if (importedNames[i - 1] === importedNames[i]) { importedNames.splice(i, 1); } } // Scans for exports definitions in the body. var exportedNames = []; re = /\bexports.(\w+)\s*=\s/g; while ((m = re.exec(body))) { exportedNames.push(m[1]); } return { amdId: amdId, amdImports: amdImports, cjsRequires: cjsRequires, jsRootName: jsRootName, jsImports: jsImports, imports: imports, importedNames: importedNames, exportedNames: exportedNames, body: body }; } /** * Reads and parses all JavaScript root files dependencies and calculates * evaluation/load order. * @param {Array} rootPaths Array of the paths for JavaScript files. * @returns {{modules: null, loadOrder: Array}} */ function readDependencies(rootPaths) { // Reading of dependencies. var modules = Object.create(null); var processed = Object.create(null); var queue = []; rootPaths.forEach(function (i) { if (processed[i]) { return; } queue.push(i); processed[i] = true; }); while (queue.length > 0) { var p = queue.shift(); var umd; try { umd = parseUmd(p); } catch (_) { // Ignoring bad UMD modules. continue; } modules[umd.amdId] = { dependencies: umd.amdImports }; umd.cjsRequires.forEach(function (r) { if (r[0] !== '.' || !/\.js$/.test(r)) { return; // not pdfjs module } var dependencyPath = path.join(path.dirname(p), r); if (processed[dependencyPath]) { return; } queue.push(dependencyPath); processed[dependencyPath] = true; }); } // Topological sorting, somewhat Kahn's algorithm but sorts found nodes at // each iteration. processed = Object.create(null); var left = [], result = []; for (var i in modules) { var hasDependencies = modules[i].dependencies.length > 0; if (hasDependencies) { left.push(i); } else { processed[i] = true; result.push(i); } } result.sort(); while (left.length > 0) { var discovered = []; left.forEach(function (i) { // Finding if we did not process all dependencies for current module yet. var hasDependecies = modules[i].dependencies.some(function (j) { return !processed[j] && !!modules[j]; }); if (!hasDependecies) { discovered.push(i); } }); if (discovered.length === 0) { throw new Error ('Some circular references exist: somewhere at ' + left.join(',')); } discovered.sort(); discovered.forEach(function (i) { result.push(i); left.splice(left.indexOf(i), 1); processed[i] = true; }); } return {modules: modules, loadOrder: result}; } /** * Validates individual file. See rules above. */ function validateFile(path, name, context) { function info(msg) { context.infoCallback(path + ': ' + msg); } function warn(msg) { context.warnCallback(path + ': ' + msg); } function error(msg) { context.errorCallback(path + ': ' + msg); } try { var umd = parseUmd(path); info('found ' + umd.amdId); if (name !== umd.amdId) { error('AMD name does not match module name'); } if (name.replace(/[_\/]/g, '') !== umd.jsRootName.toLowerCase()) { error('root name does not look like module name'); } if (umd.amdImports.length > umd.imports.length) { error('AMD imports has more entries than body imports'); } if (umd.cjsRequires.length > umd.imports.length) { error('CommonJS imports has more entries than body imports'); } if (umd.jsImports.length > umd.imports.length) { error('JS imports has more entries than body imports'); } var optionalArgs = umd.imports.length - Math.min(umd.amdImports.length, umd.cjsRequires.length, umd.jsImports.length); if (optionalArgs > 0) { warn('' + optionalArgs + ' optional args found: ' + umd.imports.slice(-optionalArgs)); } umd.jsImports.forEach(function (i, index) { if (i.indexOf('root.') !== 0) { if (index >= umd.jsImports.length - optionalArgs) { warn('Non-optional non-root based JS import: ' + i); } return; } i = i.substring('root.'.length); var j = umd.imports[index]; var offset = i.toLowerCase().lastIndexOf(j.toLowerCase()); if (offset + j.length !== i.length) { error('JS import name does not look like corresponding body import ' + 'name: ' + i + ' vs ' + j); } j = umd.amdImports[index]; if (j) { if (j.replace(/[_\/]/g, '') !== i.toLowerCase()) { error('JS import name does not look like corresponding AMD import ' + 'name: ' + i + ' vs ' + j); } } }); umd.cjsRequires.forEach(function (i, index) { var j = umd.amdImports[index]; if (!j) { return; // optional } var noExtension = i.replace(/\.js$/, ''); if (noExtension === i || i[0] !== '.') { warn('CommonJS shall have relative path and extension: ' + i); return; } var base = name.split('/'); base.pop(); var parts = noExtension.split('/'); if (parts[0] === '.') { parts.shift(); } while (parts[0] === '..') { parts.shift(); base.pop(); } if (j !== base.concat(parts).join('/')) { error('CommonJS path does not point to right AMD module: ' + i + ' vs ' + j); } }); umd.imports.forEach(function (i) { var prefix = i + '.'; if (umd.importedNames.every(function (j) { return j.indexOf(prefix) !== 0; })) { warn('import is not used to import names: ' + i); } }); // Recording the module exports and imports for further validation. // See validateImports and validateDependencies below. context.exports[name] = Object.create(null); umd.exportedNames.forEach(function (i) { context.exports[name][i] = true; }); context.dependencies[name] = umd.amdImports; umd.importedNames.forEach(function (i) { var parts = i.split('.'); var index = umd.imports.indexOf(parts[0]); if (index < 0 || !umd.amdImports[index]) { return; // some optional arg and not in AMD list? } var refModuleName = umd.amdImports[index]; var fromModule = context.imports[refModuleName]; if (!fromModule) { context.imports[refModuleName] = (fromModule = Object.create(null)); } var symbolRefs = fromModule[parts[1]]; if (!symbolRefs) { fromModule[parts[1]] = (symbolRefs = []); } symbolRefs.push(name); }); } catch (e) { warn(e.message); } } function findFilesInDirectory(dirPath, name, foundFiles) { fs.readdirSync(dirPath).forEach(function (file) { var filePath = dirPath + '/' + file; var stats = fs.statSync(filePath); if (stats.isFile() && /\.js$/i.test(file)) { var fileName = file.substring(0, file.lastIndexOf('.')); foundFiles.push({path: filePath, name: name + '/' + fileName}); } else if (stats.isDirectory() && /^\w+$/.test(file)) { findFilesInDirectory(filePath, name + '/' + file, foundFiles); } }); } function validateImports(context) { // Checks if some non-exported symbol was imported. for (var i in context.imports) { var exportedSymbols = context.exports[i]; if (!exportedSymbols) { context.warnCallback('Exported symbols don\'t exist for: ' + i); continue; } var importedSymbols = context.imports[i]; for (var j in importedSymbols) { if (!(j in exportedSymbols)) { context.errorCallback('The non-exported symbol is referred: ' + j + ' from ' + i + ' used in ' + importedSymbols[j]); } } } } function validateDependencies(context) { // Checks for circular dependency (non-efficient algorithm but does the work). var nonRoots = Object.create(null); var i, j, item; for (i in context.dependencies) { var checked = Object.create(null); var queue = [[i]]; while (queue.length > 0) { item = queue.shift(); j = item[0]; var dependencies = context.dependencies[j]; dependencies.forEach(function (q) { if (!(q in context.dependencies)) { context.warnCallback('Unknown dependency: ' + q); return; } var index = item.indexOf(q); if (index >= 0) { context.errorCallback('Circular dependency was found: ' + item.slice(0, index + 1).join('<-')); return; } if (q in checked) { return; } queue.push([q].concat(item)); checked[q] = i; nonRoots[q] = true; }); } } // Some root modules info. for (i in context.dependencies) { if (!(i in nonRoots)) { context.infoCallback('Root module: ' + i); } } } /** * Validates all modules/files in the specified path. The modules must be * defined using PDF.js UMD format. Results printed to console. * @param {Object} paths The map of the module path prefixes to file/directory * location. * @param {Object} options (optional) options for validation. * @returns {boolean} true if no error was found. */ function validateFiles(paths, options) { options = options || {}; var verbosity = options.verbosity === undefined ? 0 : options.verbosity; var wasErrorFound = false; var errorCallback = function (msg) { if (verbosity >= 0) { console.error('ERROR:' + msg); } wasErrorFound = true; }; var warnCallback = function (msg) { if (verbosity >= 1) { console.warn('WARNING: ' + msg); } }; var infoCallback = function (msg) { if (verbosity >= 5) { console.info('INFO: ' + msg); } }; // Finds all files. for (var name in paths) { if (!paths.hasOwnProperty(name)) { continue; } var path = paths[name]; var stats = fs.statSync(path); var foundFiles = []; if (stats.isFile()) { foundFiles.push({path: path, name: name}); } else if (stats.isDirectory()) { findFilesInDirectory(path, name, foundFiles); } } var context = { exports: Object.create(null), imports: Object.create(null), dependencies: Object.create(null), errorCallback: errorCallback, warnCallback: warnCallback, infoCallback: infoCallback }; foundFiles.forEach(function (pair) { validateFile(pair.path, pair.name, context); }); validateImports(context); validateDependencies(context); return !wasErrorFound; } exports.parseUmd = parseUmd; exports.readDependencies = readDependencies; exports.validateFiles = validateFiles;