Skip to content

Commit 9d213ce

Browse files
authored
[FEATURE] Properties File Escaping (#293)
Add processor nonAsciiEscaper which escapes non ASCII characters in resources. Add task escapeNonAsciiCharacters which uses nonAsciiEscaper to escape non ascii characters.
1 parent 2ba75af commit 9d213ce

37 files changed

+660
-54
lines changed

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module.exports = {
1919
bootstrapHtmlTransformer: require("./lib/processors/bootstrapHtmlTransformer"),
2020
debugFileCreator: require("./lib/processors/debugFileCreator"),
2121
resourceCopier: require("./lib/processors/resourceCopier"),
22+
nonAsciiEscaper: require("./lib/processors/nonAsciiEscaper"),
2223
stringReplacer: require("./lib/processors/stringReplacer"),
2324
themeBuilder: require("./lib/processors/themeBuilder"),
2425
uglifier: require("./lib/processors/uglifier"),
@@ -43,6 +44,7 @@ module.exports = {
4344
generateApiIndex: require("./lib/tasks/jsdoc/generateApiIndex"),
4445
generateJsdoc: require("./lib/tasks/jsdoc/generateJsdoc"),
4546
generateVersionInfo: require("./lib/tasks/generateVersionInfo"),
47+
escapeNonAsciiCharacters: require("./lib/tasks/escapeNonAsciiCharacters"),
4648
replaceCopyright: require("./lib/tasks/replaceCopyright"),
4749
replaceVersion: require("./lib/tasks/replaceVersion"),
4850
transformBootstrapHtml: require("./lib/tasks/transformBootstrapHtml"),

lib/lbt/bundle/AutoSplitter.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
const uglify = require("uglify-es");
44
const {pd} = require("pretty-data");
5+
56
const ModuleName = require("../utils/ModuleName");
67
const {SectionType} = require("./BundleDefinition");
8+
const escapePropertiesFile = require("../utils/escapePropertiesFile");
79
const log = require("@ui5/logger").getLogger("lbt:bundle:AutoSplitter");
810

911
const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9/i;
@@ -217,7 +219,6 @@ class AutoSplitter {
217219
// trace.debug("analyzed %s:%d%n", module, mw.getTargetLength());
218220
return fileContent.length;
219221
} else if ( /\.properties$/.test(module) ) {
220-
const fileContent = await resource.buffer();
221222
/* NODE-TODO minimize *.properties
222223
Properties props = new Properties();
223224
props.load(in);
@@ -226,7 +227,12 @@ class AutoSplitter {
226227
props.store(out, "");
227228
return out.toString().length();
228229
*/
229-
return fileContent.toString("latin1").length;
230+
231+
// Since AutoSplitter is also used when splitting non-project resources (e.g. dependencies)
232+
// *.properties files should be escaped if encoding option is specified
233+
const fileContent = await escapePropertiesFile(resource);
234+
235+
return fileContent.length;
230236
} else if ( this.optimizeXMLViews && /\.view.xml$/.test(module) ) {
231237
// needs to be activated when it gets activated in JSMergedModuleBuilderExt
232238
let fileContent = await resource.buffer();

lib/lbt/bundle/Builder.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {Syntax} = esprima;
1212
const {isMethodCall} = require("../utils/ASTUtils");
1313
const ModuleName = require("../utils/ModuleName");
1414
const UI5ClientConstants = require("../UI5ClientConstants");
15+
const escapePropertiesFile = require("../utils/escapePropertiesFile");
1516

1617
const BundleResolver = require("./Resolver");
1718
const BundleSplitter = require("./AutoSplitter");
@@ -442,9 +443,11 @@ class BundleBuilder {
442443
}
443444
outW.write( makeStringLiteral( fileContent ) );
444445
} else if ( /\.properties$/.test(module) ) {
445-
// same as for other text files, but input encoding is ISO_8859_1
446-
const fileContent = await resource.buffer();
447-
outW.write( makeStringLiteral( fileContent.toString("latin1") ) );
446+
// Since the Builder is also used when building non-project resources (e.g. dependencies)
447+
// *.properties files should be escaped if encoding option is specified
448+
const fileContent = await escapePropertiesFile(resource);
449+
450+
outW.write( makeStringLiteral( fileContent ) );
448451
} else {
449452
log.error("don't know how to embed module " + module); // TODO throw?
450453
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const nonAsciiEscaper = require("../../processors/nonAsciiEscaper");
2+
3+
/**
4+
* Can be used to escape *.properties files.
5+
*
6+
* Input encoding is read from project configuration.
7+
* In case the resource belongs to no project (e.g. bundler is used standalone) the default is "ISO-8859-1".
8+
*
9+
* @private
10+
* @param {Resource} resource the resource for which the content will be escaped
11+
* @returns {Promise<string>} resolves with the escaped string content of the given Resource
12+
*/
13+
module.exports = async function(resource) {
14+
const propertiesFileSourceEncoding = resource.getProject()
15+
&& resource.getProject().resources
16+
&& resource.getProject().resources.configuration
17+
&& resource.getProject().resources.configuration.propertiesFileSourceEncoding;
18+
const encoding = nonAsciiEscaper.getEncodingFromAlias(propertiesFileSourceEncoding || "ISO-8859-1");
19+
await nonAsciiEscaper({
20+
resources: [resource.resource],
21+
options: {
22+
encoding
23+
}
24+
});
25+
26+
const fileContent = await resource.buffer();
27+
28+
return fileContent.toString();
29+
};

lib/processors/bundlers/moduleBundler.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class LocatorResource extends Resource {
1616
buffer() {
1717
return this.resource.getBuffer();
1818
}
19+
20+
getProject() {
21+
return this.resource._project;
22+
}
1923
}
2024

2125
class LocatorResourcePool extends ResourcePool {

lib/processors/nonAsciiEscaper.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const escapeUnicode = require("escape-unicode");
2+
3+
/**
4+
* @see https://en.wikipedia.org/wiki/ASCII
5+
* ascii contains 128 characters.
6+
* its char codes reach from 0 to 127.
7+
* @type {number}
8+
*/
9+
const CHAR_CODE_OF_LAST_ASCII_CHARACTER = 127;
10+
11+
// use memoization for escapeUnicode function for performance
12+
const memoizeEscapeUnicodeMap = {};
13+
const memoizeEscapeUnicode = function(sChar) {
14+
if (memoizeEscapeUnicodeMap[sChar]) {
15+
return memoizeEscapeUnicodeMap[sChar];
16+
}
17+
memoizeEscapeUnicodeMap[sChar] = escapeUnicode(sChar);
18+
return memoizeEscapeUnicodeMap[sChar];
19+
};
20+
21+
/**
22+
* Escapes non ASCII characters with unicode escape sequences.
23+
*
24+
* @see https://en.wikipedia.org/wiki/ASCII
25+
* @see https://tools.ietf.org/html/rfc5137#section-6.1
26+
*
27+
*
28+
* @param {string} string input string with non ascii characters, e.g. L♥VE
29+
* @returns {{string: (string), modified: boolean}} output string with all non ascii
30+
* characters being escaped by unicode sequence, e.g. L\u2665VE
31+
*/
32+
const escapeNonAscii = function(string) {
33+
let result = "";
34+
let modified = false;
35+
for (let i = 0; i < string.length; i++) {
36+
const char = string[i];
37+
// check for non ascii characters (characters which have a char code
38+
// greater than the ascii character code range)
39+
if (string.charCodeAt(i) > CHAR_CODE_OF_LAST_ASCII_CHARACTER) {
40+
result += memoizeEscapeUnicode(char);
41+
modified = true;
42+
} else {
43+
result += char;
44+
}
45+
}
46+
return {
47+
modified,
48+
string: result
49+
};
50+
};
51+
52+
/**
53+
* Escapes non ASCII characters with unicode escape sequences.
54+
*
55+
* @example
56+
* const encoding = nonAsciiEscaper.getEncodingFromAlias("ISO-8859-1");
57+
* nonAsciiEscaper({resources, options: {encoding}});
58+
*
59+
*
60+
* @public
61+
* @alias module:@ui5/builder.processors.nonAsciiEscaper
62+
* @param {Object} parameters Parameters
63+
* @param {module:@ui5/fs.Resource[]} parameters.resources List of resources to be processed
64+
* @param {Object} [parameters.options] Options
65+
* @param {string} [parameters.options.encoding="utf8"] resource file encoding (node.js based encodings). Use #getEncodingFromAlias to get the encoding string
66+
* {@link https://nodejs.org/api/buffer.html#buffer_buffers_and_character_encodings Node.js character encodings};
67+
* @returns {Promise<module:@ui5/fs.Resource[]>} Promise resolving with the processed resources
68+
*/
69+
module.exports = async function nonAsciiEscaper({resources, options={}}) {
70+
const encoding = options.encoding || "utf8";
71+
72+
async function processResource(resource) {
73+
const resourceString = (await resource.getBuffer()).toString(encoding);
74+
const escaped = escapeNonAscii(resourceString);
75+
// only modify the resource's string if it was changed
76+
if (escaped.modified) {
77+
resource.setString(escaped.string);
78+
}
79+
return resource;
80+
}
81+
82+
return Promise.all(resources.map(processResource));
83+
};
84+
85+
const encodingMap = {
86+
"UTF-8": "utf8",
87+
"ISO-8859-1": "latin1",
88+
};
89+
90+
/**
91+
* Provides a mapping from user-friendly encoding name (alias) such as "UTF-8" and "ISO-8859-1" to node
92+
* specific encoding name such as "utf8" or "latin1". Simplifies usage of nonAsciiEscaper encoding
93+
* option such that it can be used standalone without the respective task (e.g. in Splitter, Bundler and related projects).
94+
*
95+
* @param {string} encoding encoding labels: "UTF-8" and "ISO-8859-1"
96+
* @returns {string} node.js character encoding string, e.g. utf8 and latin1
97+
*/
98+
module.exports.getEncodingFromAlias = function(encoding) {
99+
if (!encodingMap[encoding]) {
100+
throw new Error(`Encoding "${encoding}" is not supported. Only ${Object.keys(encodingMap).join(", ")} are allowed values` );
101+
}
102+
return encodingMap[encoding];
103+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const nonAsciiEscaper = require("../processors/nonAsciiEscaper");
2+
3+
/**
4+
* Task to escape non ascii characters in properties files resources.
5+
*
6+
* @public
7+
* @alias module:@ui5/builder.tasks.escapeNonAsciiCharacters
8+
* @param {Object} parameters Parameters
9+
* @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files
10+
* @param {Object} parameters.options Options
11+
* @param {string} parameters.options.pattern Glob pattern to locate the files to be processed
12+
* @param {string} parameters.options.encoding source file encoding either "UTF-8" or "ISO-8859-1"
13+
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
14+
*/
15+
module.exports = async function({workspace, options}) {
16+
if (!options.encoding) {
17+
throw new Error("[escapeNonAsciiCharacters] Mandatory option 'encoding' not provided");
18+
}
19+
20+
const allResources = await workspace.byGlob(options.pattern);
21+
22+
const processedResources = await nonAsciiEscaper({
23+
resources: allResources,
24+
options: {
25+
encoding: nonAsciiEscaper.getEncodingFromAlias(options.encoding)
26+
}
27+
});
28+
29+
await Promise.all(processedResources.map((resource) => workspace.write(resource)));
30+
};

lib/tasks/taskRepository.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const tasks = {
22
replaceCopyright: require("./replaceCopyright"),
33
replaceVersion: require("./replaceVersion"),
44
createDebugFiles: require("./createDebugFiles"),
5+
escapeNonAsciiCharacters: require("./escapeNonAsciiCharacters"),
56
executeJsdocSdkTransformation: require("./jsdoc/executeJsdocSdkTransformation"),
67
generateApiIndex: require("./jsdoc/generateApiIndex"),
78
generateJsdoc: require("./jsdoc/generateJsdoc"),

lib/types/application/ApplicationBuilder.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const tasks = { // can't require index.js due to circular dependency
88
generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"),
99
generateBundle: require("../../tasks/bundlers/generateBundle"),
1010
generateCachebusterInfo: require("../../tasks/generateCachebusterInfo"),
11+
escapeNonAsciiCharacters: require("../../tasks/escapeNonAsciiCharacters"),
1112
buildThemes: require("../../tasks/buildThemes"),
1213
createDebugFiles: require("../../tasks/createDebugFiles"),
1314
generateVersionInfo: require("../../tasks/generateVersionInfo"),
@@ -26,6 +27,19 @@ class ApplicationBuilder extends AbstractBuilder {
2627
"Also see: https://github.com/SAP/ui5-builder#application");
2728
}
2829

30+
this.addTask("escapeNonAsciiCharacters", () => {
31+
const propertiesFileSourceEncoding = project.resources
32+
&& project.resources.configuration
33+
&& project.resources.configuration.propertiesFileSourceEncoding;
34+
return tasks.escapeNonAsciiCharacters({
35+
workspace: resourceCollections.workspace,
36+
options: {
37+
encoding: propertiesFileSourceEncoding,
38+
pattern: "/**/*.properties"
39+
}
40+
});
41+
});
42+
2943
this.addTask("replaceCopyright", () => {
3044
return tasks.replaceCopyright({
3145
workspace: resourceCollections.workspace,

lib/types/application/ApplicationFormatter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ class ApplicationFormatter extends AbstractUi5Formatter {
133133
project.resources.configuration.paths.webapp = "webapp";
134134
}
135135

136+
// default encoding to "ISO-8859-1" if not specified
137+
if (!project.resources.configuration.propertiesFileSourceEncoding) {
138+
project.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1";
139+
}
140+
if (!["ISO-8859-1", "UTF-8"].includes(project.resources.configuration.propertiesFileSourceEncoding)) {
141+
throw new Error(`Invalid properties file encoding specified for project ${project.id}: ` +
142+
`encoding provided: ${project.resources.configuration.propertiesFileSourceEncoding}. Must be either "ISO-8859-1" or "UTF-8".`);
143+
}
144+
136145
const absolutePath = path.join(project.path, project.resources.configuration.paths.webapp);
137146
return this.dirExists(absolutePath).then((bExists) => {
138147
if (!bExists) {

0 commit comments

Comments
 (0)