blob: 1143d1bc79e9cfe8d30bc482b170380c85f003fd [file] [log] [blame]
// Copyright 2020 Google LLC
//
// 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
//
// https://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.
import 'package:yaml/yaml.dart';
import 'utils.dart';
/// Given [value], tries to format it into a plain string recognizable by YAML.
/// If it fails, it defaults to returning a double-quoted string.
///
/// Not all values can be formatted into a plain string. If the string contains
/// an escape sequence, it can only be detected when in a double-quoted
/// sequence. Plain strings may also be misinterpreted by the YAML parser (e.g.
/// ' null').
String _tryYamlEncodePlain(Object? value) {
if (value is YamlNode) {
AssertionError(
'YamlNodes should not be passed directly into getSafeString!');
}
assertValidScalar(value);
if (value is String) {
/// If it contains a dangerous character we want to wrap the result with
/// double quotes because the double quoted style allows for arbitrary
/// strings with "\" escape sequences.
///
/// See 7.3.1 Double-Quoted Style
/// https://yaml.org/spec/1.2/spec.html#id2787109
if (isDangerousString(value)) {
return _yamlEncodeDoubleQuoted(value);
}
return value;
}
return value.toString();
}
/// Checks if [string] has unprintable characters according to
/// [unprintableCharCodes].
bool _hasUnprintableCharacters(String string) {
final codeUnits = string.codeUnits;
for (final key in unprintableCharCodes.keys) {
if (codeUnits.contains(key)) return true;
}
return false;
}
/// Generates a YAML-safe double-quoted string based on [string], escaping the
/// list of characters as defined by the YAML 1.2 spec.
///
/// See 5.7 Escaped Characters https://yaml.org/spec/1.2/spec.html#id2776092
String _yamlEncodeDoubleQuoted(String string) {
final buffer = StringBuffer();
for (final codeUnit in string.codeUnits) {
if (doubleQuoteEscapeChars[codeUnit] != null) {
buffer.write(doubleQuoteEscapeChars[codeUnit]);
} else {
buffer.writeCharCode(codeUnit);
}
}
return '"$buffer"';
}
/// Generates a YAML-safe single-quoted string. Automatically escapes
/// single-quotes.
///
/// It is important that we ensure that [string] is free of unprintable
/// characters by calling [assertValidScalar] before invoking this function.
String _tryYamlEncodeSingleQuoted(String string) {
final result = string.replaceAll('\'', '\'\'');
return '\'$result\'';
}
/// Generates a YAML-safe folded string.
///
/// It is important that we ensure that [string] is free of unprintable
/// characters by calling [assertValidScalar] before invoking this function.
String _tryYamlEncodeFolded(String string, int indentation, String lineEnding) {
String result;
final trimmedString = string.trimRight();
final removedPortion = string.substring(trimmedString.length);
if (removedPortion.contains('\n')) {
result = '>+\n' + ' ' * indentation;
} else {
result = '>-\n' + ' ' * indentation;
}
/// Duplicating the newline for folded strings preserves it in YAML.
/// Assumes the user did not try to account for windows documents by using
/// `\r\n` already
return result +
trimmedString.replaceAll('\n', lineEnding * 2 + ' ' * indentation) +
removedPortion;
}
/// Generates a YAML-safe literal string.
///
/// It is important that we ensure that [string] is free of unprintable
/// characters by calling [assertValidScalar] before invoking this function.
String _tryYamlEncodeLiteral(
String string, int indentation, String lineEnding) {
final result = '|-\n$string';
/// Assumes the user did not try to account for windows documents by using
/// `\r\n` already
return result.replaceAll('\n', lineEnding + ' ' * indentation);
}
/// Returns [value] with the necessary formatting applied in a flow context
/// if possible.
///
/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
/// possible. Certain cases make this impossible (e.g. a plain string scalar that
/// starts with '>'), in which case we will produce [value] with default styling
/// options.
String _yamlEncodeFlowScalar(YamlNode value) {
if (value is YamlScalar) {
assertValidScalar(value.value);
if (value.value is String) {
if (_hasUnprintableCharacters(value.value) ||
value.style == ScalarStyle.DOUBLE_QUOTED) {
return _yamlEncodeDoubleQuoted(value.value);
}
if (value.style == ScalarStyle.SINGLE_QUOTED) {
return _tryYamlEncodeSingleQuoted(value.value);
}
}
return _tryYamlEncodePlain(value.value);
}
assertValidScalar(value);
return _tryYamlEncodePlain(value);
}
/// Returns [value] with the necessary formatting applied in a block context
/// if possible.
///
/// If [value] is a [YamlScalar], we try to respect its [style] parameter where
/// possible. Certain cases make this impossible (e.g. a folded string scalar
/// 'null'), in which case we will produce [value] with default styling
/// options.
String yamlEncodeBlockScalar(
YamlNode value, int indentation, String lineEnding) {
if (value is YamlScalar) {
assertValidScalar(value.value);
if (value.value is String) {
if (_hasUnprintableCharacters(value.value)) {
return _yamlEncodeDoubleQuoted(value.value);
}
if (value.style == ScalarStyle.SINGLE_QUOTED) {
return _tryYamlEncodeSingleQuoted(value.value);
}
// Strings with only white spaces will cause a misparsing
if (value.value.trim().length == value.value.length &&
value.value.length != 0) {
if (value.style == ScalarStyle.FOLDED) {
return _tryYamlEncodeFolded(value.value, indentation, lineEnding);
}
if (value.style == ScalarStyle.LITERAL) {
return _tryYamlEncodeLiteral(value.value, indentation, lineEnding);
}
}
}
return _tryYamlEncodePlain(value.value);
}
assertValidScalar(value);
/// The remainder of the possibilities are similar to how [getFlowScalar]
/// treats [value].
return _yamlEncodeFlowScalar(value);
}
/// Returns [value] with the necessary formatting applied in a flow context.
///
/// If [value] is a [YamlNode], we try to respect its [style] parameter where
/// possible. Certain cases make this impossible (e.g. a plain string scalar
/// that starts with '>', a child having a block style parameters), in which
/// case we will produce [value] with default styling options.
String yamlEncodeFlowString(YamlNode value) {
if (value is YamlList) {
final list = value.nodes;
final safeValues = list.map(yamlEncodeFlowString);
return '[' + safeValues.join(', ') + ']';
} else if (value is YamlMap) {
final safeEntries = value.nodes.entries.map((entry) {
final safeKey = yamlEncodeFlowString(entry.key);
final safeValue = yamlEncodeFlowString(entry.value);
return '$safeKey: $safeValue';
});
return '{' + safeEntries.join(', ') + '}';
}
return _yamlEncodeFlowScalar(value);
}
/// Returns [value] with the necessary formatting applied in a block context.
///
/// If [value] is a [YamlNode], we respect its [style] parameter.
String yamlEncodeBlockString(
YamlNode value, int indentation, String lineEnding) {
const additionalIndentation = 2;
if (!isBlockNode(value)) return yamlEncodeFlowString(value);
final newIndentation = indentation + additionalIndentation;
if (value is YamlList) {
if (value.isEmpty) return ' ' * indentation + '[]';
Iterable<String> safeValues;
final children = value.nodes;
safeValues = children.map((child) {
var valueString =
yamlEncodeBlockString(child, newIndentation, lineEnding);
if (isCollection(child) && !isFlowYamlCollectionNode(child)) {
valueString = valueString.substring(newIndentation);
}
return ' ' * indentation + '- $valueString';
});
return safeValues.join(lineEnding);
} else if (value is YamlMap) {
if (value.isEmpty) return ' ' * indentation + '{}';
return value.nodes.entries.map((entry) {
final safeKey = yamlEncodeFlowString(entry.key);
final formattedKey = ' ' * indentation + safeKey;
final formattedValue =
yamlEncodeBlockString(entry.value, newIndentation, lineEnding);
/// Empty collections are always encoded in flow-style, so new-line must
/// be avoided
if (isCollection(entry.value) && !isEmpty(entry.value)) {
return formattedKey + ':\n' + formattedValue;
}
return formattedKey + ': ' + formattedValue;
}).join(lineEnding);
}
return yamlEncodeBlockScalar(value, newIndentation, lineEnding);
}
/// List of unprintable characters.
///
/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
final Map<int, String> unprintableCharCodes = {
0: '\\0', // Escaped ASCII null (#x0) character.
7: '\\a', // Escaped ASCII bell (#x7) character.
8: '\\b', // Escaped ASCII backspace (#x8) character.
11: '\\v', // Escaped ASCII vertical tab (#xB) character.
12: '\\f', // Escaped ASCII form feed (#xC) character.
13: '\\r', // Escaped ASCII carriage return (#xD) character. Line Break.
27: '\\e', // Escaped ASCII escape (#x1B) character.
133: '\\N', // Escaped Unicode next line (#x85) character.
160: '\\_', // Escaped Unicode non-breaking space (#xA0) character.
8232: '\\L', // Escaped Unicode line separator (#x2028) character.
8233: '\\P', // Escaped Unicode paragraph separator (#x2029) character.
};
/// List of escape characters. In particular, \x32 is not included because it
/// can be processed normally.
///
/// See 5.7 Escape Characters https://yaml.org/spec/1.2/spec.html#id2776092
final Map<int, String> doubleQuoteEscapeChars = {
...unprintableCharCodes,
9: '\\t', // Escaped ASCII horizontal tab (#x9) character. Printable
10: '\\n', // Escaped ASCII line feed (#xA) character. Line Break.
34: '\\"', // Escaped ASCII double quote (#x22).
47: '\\/', // Escaped ASCII slash (#x2F), for JSON compatibility.
92: '\\\\', // Escaped ASCII back slash (#x5C).
};