/
githubmirror
/
brackets
Обзор
Документация
Войти
/
githubmirror
/
brackets
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/extensibility/node/package-validator.js
371 строка
14 KB
ficristo
On Windows force a 32 bit build until nodejs 64 bit is supported (#13384)
05 авг 2017, 11:57
05 авг 2017, 11:57
449299b
Код
Авторство
О чём код?
/* * Copyright (c) 2013 - present Adobe Systems Incorporated. All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * */ /*eslint-env node */ /*jslint node: true, regexp: true */ "use strict"; var DecompressZip = require("decompress-zip"), semver = require("semver"), path = require("path"), temp = require("temp"), fs = require("fs-extra"), performNpmInstallIfRequired = require("./npm-installer").performNpmInstallIfRequired; // Track and cleanup files at exit temp.track(); var Errors = { NOT_FOUND_ERR: "NOT_FOUND_ERR", // {0} is path where ZIP file was expected INVALID_ZIP_FILE: "INVALID_ZIP_FILE", // {0} is path to ZIP file INVALID_PACKAGE_JSON: "INVALID_PACKAGE_JSON", // {0} is JSON parse error, {1} is path to ZIP file MISSING_PACKAGE_NAME: "MISSING_PACKAGE_NAME", // {0} is path to ZIP file BAD_PACKAGE_NAME: "BAD_PACKAGE_NAME", // {0} is the name MISSING_PACKAGE_VERSION: "MISSING_PACKAGE_VERSION", // {0} is path to ZIP file INVALID_VERSION_NUMBER: "INVALID_VERSION_NUMBER", // {0} is version string in JSON, {1} is path to ZIP file MISSING_MAIN: "MISSING_MAIN", // {0} is path to ZIP file MISSING_PACKAGE_JSON: "MISSING_PACKAGE_JSON", // {0} is path to ZIP file INVALID_BRACKETS_VERSION: "INVALID_BRACKETS_VERSION", // {0} is the version string in JSON, {1} is the path to the zip file, DISALLOWED_WORDS: "DISALLOWED_WORDS" // {0} is the field with the word, {1} is a string list of words that were in violation, {2} is the path to the zip file }; /* * Directories to ignore when determining whether the contents of an extension are * in a subfolder. */ var ignoredFolders = [ "__MACOSX" ]; /** * Returns true if the name presented is acceptable as a package name. This enforces the * requirement as presented in the CommonJS spec: http://wiki.commonjs.org/wiki/Packages/1.0 * which states: * * "This must be a unique, lowercase alpha-numeric name without spaces. It may include "." or "_" or "-" characters." * * We add the additional requirement that the first character must be a letter or number * (there's a security implication to allowing a name like "..", because the name is * used in directory names). * * @param {string} name to test * @return {boolean} true if the name is valid */ function validateName(name) { if (/^[a-z0-9][a-z0-9._\-]*$/.exec(name)) { return true; } return false; } // Parses strings of the form "name <email> (url)" where email and url are optional var _personRegex = /^([^<\(]+)(?:\s+<([^>]+)>)?(?:\s+\(([^\)]+)\))?$/; /** * Normalizes person fields from package.json. * * These fields can be an object with name, email and url properties or a * string of the form "name <email> <url>". This does a tolerant parsing of * the data to try to return an object with name and optional email and url. * If the string does not match the format, the string is returned as the * name on the resulting object. * * If an object other than a string is passed in, it's returned as is. * * @param <String|Object> obj to normalize * @return {Object} person object with name and optional email and url */ function parsePersonString(obj) { if (typeof (obj) === "string") { var parts = _personRegex.exec(obj); // No regex match, so we just synthesize an object with an opaque name string if (!parts) { return { name: obj }; } else { var result = { name: parts[1] }; if (parts[2]) { result.email = parts[2]; } if (parts[3]) { result.url = parts[3]; } return result; } } else { // obj is not a string, so return as is return obj; } } /** * Determines if any of the words in wordlist appear in str. * * @param {String[]} wordlist list of words to check * @param {String} str to check for words * @return {String[]} words that matched */ function containsWords(wordlist, str) { var i; var matches = []; for (i = 0; i < wordlist.length; i++) { var re = new RegExp("\\b" + wordlist[i] + "\\b", "i"); if (re.exec(str)) { matches.push(wordlist[i]); } } return matches; } /** * Finds the common prefix, if any, for the files in a package file. * * In some package files, all of the files are contained in a subdirectory, and this function * will identify that directory if it exists. * * @param {string} extractDir directory into which the package was extracted * @param {function(Error, string)} callback function to accept err, commonPrefix (which will be "" if there is none) */ function findCommonPrefix(extractDir, callback) { fs.readdir(extractDir, function (err, files) { ignoredFolders.forEach(function (folder) { var index = files.indexOf(folder); if (index !== -1) { files.splice(index, 1); } }); if (err) { callback(err); } else if (files.length === 1) { var name = files[0]; if (fs.statSync(path.join(extractDir, name)).isDirectory()) { callback(null, name); } else { callback(null, ""); } } else { callback(null, ""); } }); } /** * Validates the contents of package.json. * * @param {string} path path to package file (used in error reporting) * @param {string} packageJSON path to the package.json file to check * @param {Object} options validation options passed to `validate()` * @param {function(Error, Array.<Array.<string, ...>>, Object)} callback function to call with array of errors and metadata */ function validatePackageJSON(path, packageJSON, options, callback) { var errors = []; if (fs.existsSync(packageJSON)) { fs.readFile(packageJSON, { encoding: "utf8" }, function (err, data) { if (err) { callback(err, null, null); return; } var metadata; try { metadata = JSON.parse(data); } catch (e) { errors.push([Errors.INVALID_PACKAGE_JSON, e.toString(), path]); callback(null, errors, undefined); return; } // confirm required fields in the metadata if (!metadata.name) { errors.push([Errors.MISSING_PACKAGE_NAME, path]); } else if (!validateName(metadata.name)) { errors.push([Errors.BAD_PACKAGE_NAME, metadata.name]); } if (!metadata.version) { errors.push([Errors.MISSING_PACKAGE_VERSION, path]); } else if (!semver.valid(metadata.version)) { errors.push([Errors.INVALID_VERSION_NUMBER, metadata.version, path]); } // normalize the author if (metadata.author) { metadata.author = parsePersonString(metadata.author); } // contributors should be an array of people. // normalize each entry. if (metadata.contributors) { if (metadata.contributors.map) { metadata.contributors = metadata.contributors.map(function (person) { return parsePersonString(person); }); } else { metadata.contributors = [ parsePersonString(metadata.contributors) ]; } } if (metadata.engines && metadata.engines.brackets) { var range = metadata.engines.brackets; if (!semver.validRange(range)) { errors.push([Errors.INVALID_BRACKETS_VERSION, range, path]); } } if (options.disallowedWords) { ["title", "description", "name"].forEach(function (field) { var words = containsWords(options.disallowedWords, metadata[field]); if (words.length > 0) { errors.push([Errors.DISALLOWED_WORDS, field, words.toString(), path]); } }); } callback(null, errors, metadata); }); } else { if (options.requirePackageJSON) { errors.push([Errors.MISSING_PACKAGE_JSON, path]); } callback(null, errors, null); } } /** * Extracts the package into the given directory and then validates it. * * @param {string} zipPath path to package zip file * @param {string} extractDir directory to extract package into * @param {Object} options validation options * @param {function(Error, {errors: Array, metadata: Object, commonPrefix: string, extractDir: string})} callback function to call with the result */ function extractAndValidateFiles(zipPath, extractDir, options, callback) { var unzipper = new DecompressZip(zipPath); unzipper.on("error", function (err) { // General error to report for problems reading the file callback(null, { errors: [[Errors.INVALID_ZIP_FILE, zipPath, err]] }); return; }); unzipper.on("extract", function (log) { findCommonPrefix(extractDir, function (err, commonPrefix) { if (err) { callback(err, null); return; } var packageJSON = path.join(extractDir, commonPrefix, "package.json"); validatePackageJSON(zipPath, packageJSON, options, function (err, errors, metadata) { if (err) { callback(err, null); return; } var mainJS = path.join(extractDir, commonPrefix, "main.js"), isTheme = metadata && metadata.theme; // Throw missing main.js file only for non-theme extensions if (!isTheme && !fs.existsSync(mainJS)) { errors.push([Errors.MISSING_MAIN, zipPath, mainJS]); } var npmOptions = ['--production']; if (options.proxy) { npmOptions.push('--proxy ' + options.proxy); } if (process.platform.startsWith('win')) { // On Windows force a 32 bit build until nodejs 64 bit is supported. npmOptions.push('--arch=ia32'); npmOptions.push('--npm_config_arch=ia32'); npmOptions.push('--npm_config_target_arch=ia32'); } performNpmInstallIfRequired(npmOptions, { errors: errors, metadata: metadata, commonPrefix: commonPrefix, extractDir: extractDir }, callback); }); }); }); unzipper.extract({ path: extractDir, filter: function (file) { return file.type !== "SymbolicLink"; } }); } /** * Implements the "validate" command in the "extensions" domain. * Validates the zipped package at path. * * The "err" parameter of the callback is only set if there was an * unexpected error. Otherwise, errors are reported in the result. * * The result object has an "errors" property. It is an array of * arrays of strings. Each array in the array is a set of parameters * that can be passed to StringUtils.format for internationalization. * The array will be empty if there are no errors. * * The result will have a "metadata" property if the metadata was * read successfully from package.json in the zip file. * * @param {string} path Absolute path to the package zip file * @param {{requirePackageJSON: ?boolean, disallowedWords: ?Array.<string>, proxy: ?<string>}} options for validation * @param {function} callback (err, result) */ function validate(path, options, callback) { options = options || {}; fs.exists(path, function (doesExist) { if (!doesExist) { callback(null, { errors: [[Errors.NOT_FOUND_ERR, path]] }); return; } temp.mkdir("bracketsPackage_", function _tempDirCreated(err, extractDir) { if (err) { callback(err, null); return; } extractAndValidateFiles(path, extractDir, options, callback); }); }); } // exported for unit testing exports._parsePersonString = parsePersonString; exports.errors = Errors; exports.validate = validate;