Skip to content

Commit

Permalink
[FEATURE] Properties File Escaping (#293)
Browse files Browse the repository at this point in the history
Add processor nonAsciiEscaper which escapes non ASCII characters in resources.
Add task escapeNonAsciiCharacters which uses nonAsciiEscaper to escape
non ascii characters.
  • Loading branch information
tobiasso85 authored Jul 29, 2019
1 parent 2ba75af commit 9d213ce
Show file tree
Hide file tree
Showing 37 changed files with 660 additions and 54 deletions.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
bootstrapHtmlTransformer: require("./lib/processors/bootstrapHtmlTransformer"),
debugFileCreator: require("./lib/processors/debugFileCreator"),
resourceCopier: require("./lib/processors/resourceCopier"),
nonAsciiEscaper: require("./lib/processors/nonAsciiEscaper"),
stringReplacer: require("./lib/processors/stringReplacer"),
themeBuilder: require("./lib/processors/themeBuilder"),
uglifier: require("./lib/processors/uglifier"),
Expand All @@ -43,6 +44,7 @@ module.exports = {
generateApiIndex: require("./lib/tasks/jsdoc/generateApiIndex"),
generateJsdoc: require("./lib/tasks/jsdoc/generateJsdoc"),
generateVersionInfo: require("./lib/tasks/generateVersionInfo"),
escapeNonAsciiCharacters: require("./lib/tasks/escapeNonAsciiCharacters"),
replaceCopyright: require("./lib/tasks/replaceCopyright"),
replaceVersion: require("./lib/tasks/replaceVersion"),
transformBootstrapHtml: require("./lib/tasks/transformBootstrapHtml"),
Expand Down
10 changes: 8 additions & 2 deletions lib/lbt/bundle/AutoSplitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

const uglify = require("uglify-es");
const {pd} = require("pretty-data");

const ModuleName = require("../utils/ModuleName");
const {SectionType} = require("./BundleDefinition");
const escapePropertiesFile = require("../utils/escapePropertiesFile");
const log = require("@ui5/logger").getLogger("lbt:bundle:AutoSplitter");

const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9/i;
Expand Down Expand Up @@ -217,7 +219,6 @@ class AutoSplitter {
// trace.debug("analyzed %s:%d%n", module, mw.getTargetLength());
return fileContent.length;
} else if ( /\.properties$/.test(module) ) {
const fileContent = await resource.buffer();
/* NODE-TODO minimize *.properties
Properties props = new Properties();
props.load(in);
Expand All @@ -226,7 +227,12 @@ class AutoSplitter {
props.store(out, "");
return out.toString().length();
*/
return fileContent.toString("latin1").length;

// Since AutoSplitter is also used when splitting non-project resources (e.g. dependencies)
// *.properties files should be escaped if encoding option is specified
const fileContent = await escapePropertiesFile(resource);

return fileContent.length;
} else if ( this.optimizeXMLViews && /\.view.xml$/.test(module) ) {
// needs to be activated when it gets activated in JSMergedModuleBuilderExt
let fileContent = await resource.buffer();
Expand Down
9 changes: 6 additions & 3 deletions lib/lbt/bundle/Builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {Syntax} = esprima;
const {isMethodCall} = require("../utils/ASTUtils");
const ModuleName = require("../utils/ModuleName");
const UI5ClientConstants = require("../UI5ClientConstants");
const escapePropertiesFile = require("../utils/escapePropertiesFile");

const BundleResolver = require("./Resolver");
const BundleSplitter = require("./AutoSplitter");
Expand Down Expand Up @@ -442,9 +443,11 @@ class BundleBuilder {
}
outW.write( makeStringLiteral( fileContent ) );
} else if ( /\.properties$/.test(module) ) {
// same as for other text files, but input encoding is ISO_8859_1
const fileContent = await resource.buffer();
outW.write( makeStringLiteral( fileContent.toString("latin1") ) );
// Since the Builder is also used when building non-project resources (e.g. dependencies)
// *.properties files should be escaped if encoding option is specified
const fileContent = await escapePropertiesFile(resource);

outW.write( makeStringLiteral( fileContent ) );
} else {
log.error("don't know how to embed module " + module); // TODO throw?
}
Expand Down
29 changes: 29 additions & 0 deletions lib/lbt/utils/escapePropertiesFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const nonAsciiEscaper = require("../../processors/nonAsciiEscaper");

/**
* Can be used to escape *.properties files.
*
* Input encoding is read from project configuration.
* In case the resource belongs to no project (e.g. bundler is used standalone) the default is "ISO-8859-1".
*
* @private
* @param {Resource} resource the resource for which the content will be escaped
* @returns {Promise<string>} resolves with the escaped string content of the given Resource
*/
module.exports = async function(resource) {
const propertiesFileSourceEncoding = resource.getProject()
&& resource.getProject().resources
&& resource.getProject().resources.configuration
&& resource.getProject().resources.configuration.propertiesFileSourceEncoding;
const encoding = nonAsciiEscaper.getEncodingFromAlias(propertiesFileSourceEncoding || "ISO-8859-1");
await nonAsciiEscaper({
resources: [resource.resource],
options: {
encoding
}
});

const fileContent = await resource.buffer();

return fileContent.toString();
};
4 changes: 4 additions & 0 deletions lib/processors/bundlers/moduleBundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class LocatorResource extends Resource {
buffer() {
return this.resource.getBuffer();
}

getProject() {
return this.resource._project;
}
}

class LocatorResourcePool extends ResourcePool {
Expand Down
103 changes: 103 additions & 0 deletions lib/processors/nonAsciiEscaper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const escapeUnicode = require("escape-unicode");

/**
* @see https://en.wikipedia.org/wiki/ASCII
* ascii contains 128 characters.
* its char codes reach from 0 to 127.
* @type {number}
*/
const CHAR_CODE_OF_LAST_ASCII_CHARACTER = 127;

// use memoization for escapeUnicode function for performance
const memoizeEscapeUnicodeMap = {};
const memoizeEscapeUnicode = function(sChar) {
if (memoizeEscapeUnicodeMap[sChar]) {
return memoizeEscapeUnicodeMap[sChar];
}
memoizeEscapeUnicodeMap[sChar] = escapeUnicode(sChar);
return memoizeEscapeUnicodeMap[sChar];
};

/**
* Escapes non ASCII characters with unicode escape sequences.
*
* @see https://en.wikipedia.org/wiki/ASCII
* @see https://tools.ietf.org/html/rfc5137#section-6.1
*
*
* @param {string} string input string with non ascii characters, e.g. L♥VE
* @returns {{string: (string), modified: boolean}} output string with all non ascii
* characters being escaped by unicode sequence, e.g. L\u2665VE
*/
const escapeNonAscii = function(string) {
let result = "";
let modified = false;
for (let i = 0; i < string.length; i++) {
const char = string[i];
// check for non ascii characters (characters which have a char code
// greater than the ascii character code range)
if (string.charCodeAt(i) > CHAR_CODE_OF_LAST_ASCII_CHARACTER) {
result += memoizeEscapeUnicode(char);
modified = true;
} else {
result += char;
}
}
return {
modified,
string: result
};
};

/**
* Escapes non ASCII characters with unicode escape sequences.
*
* @example
* const encoding = nonAsciiEscaper.getEncodingFromAlias("ISO-8859-1");
* nonAsciiEscaper({resources, options: {encoding}});
*
*
* @public
* @alias module:@ui5/builder.processors.nonAsciiEscaper
* @param {Object} parameters Parameters
* @param {module:@ui5/fs.Resource[]} parameters.resources List of resources to be processed
* @param {Object} [parameters.options] Options
* @param {string} [parameters.options.encoding="utf8"] resource file encoding (node.js based encodings). Use #getEncodingFromAlias to get the encoding string
* {@link https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings Node.js character encodings};
* @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with the processed resources
*/
module.exports = async function nonAsciiEscaper({resources, options={}}) {
const encoding = options.encoding || "utf8";

async function processResource(resource) {
const resourceString = (await resource.getBuffer()).toString(encoding);
const escaped = escapeNonAscii(resourceString);
// only modify the resource's string if it was changed
if (escaped.modified) {
resource.setString(escaped.string);
}
return resource;
}

return Promise.all(resources.map(processResource));
};

const encodingMap = {
"UTF-8": "utf8",
"ISO-8859-1": "latin1",
};

/**
* Provides a mapping from user-friendly encoding name (alias) such as "UTF-8" and "ISO-8859-1" to node
* specific encoding name such as "utf8" or "latin1". Simplifies usage of nonAsciiEscaper encoding
* option such that it can be used standalone without the respective task (e.g. in Splitter, Bundler and related projects).
*
* @param {string} encoding encoding labels: "UTF-8" and "ISO-8859-1"
* @returns {string} node.js character encoding string, e.g. utf8 and latin1
*/
module.exports.getEncodingFromAlias = function(encoding) {
if (!encodingMap[encoding]) {
throw new Error(`Encoding "${encoding}" is not supported. Only ${Object.keys(encodingMap).join(", ")} are allowed values` );
}
return encodingMap[encoding];
};
30 changes: 30 additions & 0 deletions lib/tasks/escapeNonAsciiCharacters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const nonAsciiEscaper = require("../processors/nonAsciiEscaper");

/**
* Task to escape non ascii characters in properties files resources.
*
* @public
* @alias module:@ui5/builder.tasks.escapeNonAsciiCharacters
* @param {Object} parameters Parameters
* @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files
* @param {Object} parameters.options Options
* @param {string} parameters.options.pattern Glob pattern to locate the files to be processed
* @param {string} parameters.options.encoding source file encoding either "UTF-8" or "ISO-8859-1"
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
module.exports = async function({workspace, options}) {
if (!options.encoding) {
throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided");
}

const allResources = await workspace.byGlob(options.pattern);

const processedResources = await nonAsciiEscaper({
resources: allResources,
options: {
encoding: nonAsciiEscaper.getEncodingFromAlias(options.encoding)
}
});

await Promise.all(processedResources.map((resource) => workspace.write(resource)));
};
1 change: 1 addition & 0 deletions lib/tasks/taskRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const tasks = {
replaceCopyright: require("./replaceCopyright"),
replaceVersion: require("./replaceVersion"),
createDebugFiles: require("./createDebugFiles"),
escapeNonAsciiCharacters: require("./escapeNonAsciiCharacters"),
executeJsdocSdkTransformation: require("./jsdoc/executeJsdocSdkTransformation"),
generateApiIndex: require("./jsdoc/generateApiIndex"),
generateJsdoc: require("./jsdoc/generateJsdoc"),
Expand Down
14 changes: 14 additions & 0 deletions lib/types/application/ApplicationBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const tasks = { // can't require index.js due to circular dependency
generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"),
generateBundle: require("../../tasks/bundlers/generateBundle"),
generateCachebusterInfo: require("../../tasks/generateCachebusterInfo"),
escapeNonAsciiCharacters: require("../../tasks/escapeNonAsciiCharacters"),
buildThemes: require("../../tasks/buildThemes"),
createDebugFiles: require("../../tasks/createDebugFiles"),
generateVersionInfo: require("../../tasks/generateVersionInfo"),
Expand All @@ -26,6 +27,19 @@ class ApplicationBuilder extends AbstractBuilder {
"Also see: https://github.com/SAP/ui5-builder#application");
}

this.addTask("escapeNonAsciiCharacters", () => {
const propertiesFileSourceEncoding = project.resources
&& project.resources.configuration
&& project.resources.configuration.propertiesFileSourceEncoding;
return tasks.escapeNonAsciiCharacters({
workspace: resourceCollections.workspace,
options: {
encoding: propertiesFileSourceEncoding,
pattern: "/**/*.properties"
}
});
});

this.addTask("replaceCopyright", () => {
return tasks.replaceCopyright({
workspace: resourceCollections.workspace,
Expand Down
9 changes: 9 additions & 0 deletions lib/types/application/ApplicationFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ class ApplicationFormatter extends AbstractUi5Formatter {
project.resources.configuration.paths.webapp = "webapp";
}

// default encoding to "ISO-8859-1" if not specified
if (!project.resources.configuration.propertiesFileSourceEncoding) {
project.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1";
}
if (!["ISO-8859-1", "UTF-8"].includes(project.resources.configuration.propertiesFileSourceEncoding)) {
throw new Error(`Invalid properties file encoding specified for project ${project.id}: ` +
`encoding provided: ${project.resources.configuration.propertiesFileSourceEncoding}. Must be either "ISO-8859-1" or "UTF-8".`);
}

const absolutePath = path.join(project.path, project.resources.configuration.paths.webapp);
return this.dirExists(absolutePath).then((bExists) => {
if (!bExists) {
Expand Down
14 changes: 14 additions & 0 deletions lib/types/library/LibraryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const tasks = { // can't require index.js due to circular dependency
generateLibraryPreload: require("../../tasks/bundlers/generateLibraryPreload"),
generateManifestBundle: require("../../tasks/bundlers/generateManifestBundle"),
generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"),
escapeNonAsciiCharacters: require("../../tasks/escapeNonAsciiCharacters"),
buildThemes: require("../../tasks/buildThemes"),
createDebugFiles: require("../../tasks/createDebugFiles"),
generateJsdoc: require("../../tasks/jsdoc/generateJsdoc"),
Expand All @@ -25,6 +26,19 @@ class LibraryBuilder extends AbstractBuilder {
"Also see: https://github.com/SAP/ui5-builder#library");
}

this.addTask("escapeNonAsciiCharacters", () => {
const propertiesFileSourceEncoding = project.resources
&& project.resources.configuration
&& project.resources.configuration.propertiesFileSourceEncoding;
return tasks.escapeNonAsciiCharacters({
workspace: resourceCollections.workspace,
options: {
encoding: propertiesFileSourceEncoding,
pattern: "/**/*.properties"
}
});
});

this.addTask("replaceCopyright", () => {
const replaceCopyright = tasks.replaceCopyright;
return replaceCopyright({
Expand Down
9 changes: 9 additions & 0 deletions lib/types/library/LibraryFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,15 @@ class LibraryFormatter extends AbstractUi5Formatter {
project.resources.configuration.paths.test = "test";
}

// default encoding to "ISO-8859-1" if not specified
if (!project.resources.configuration.propertiesFileSourceEncoding) {
project.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1";
}
if (!["ISO-8859-1", "UTF-8"].includes(project.resources.configuration.propertiesFileSourceEncoding)) {
throw new Error(`Invalid properties file encoding specified for project ${project.id}: ` +
`encoding provided: ${project.resources.configuration.propertiesFileSourceEncoding}. Must be either "ISO-8859-1" or "UTF-8".`);
}

const absoluteSrcPath = path.join(project.path, project.resources.configuration.paths.src);
const absoluteTestPath = path.join(project.path, project.resources.configuration.paths.test);
return Promise.all([
Expand Down
15 changes: 10 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9d213ce

Please sign in to comment.