Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
============

* Your contribution here.
* [#139](https://github.com/jenkinsci/ansicolor-plugin/issues/139)/[#143](https://github.com/jenkinsci/ansicolor-plugin/pull/143): Prevent an `IndexOutOfBoundsException` from being thrown when two or more non-ASCII-compatible characters are present in colored text - [@dwnusbaum](https://github.com/dwnusbaum).

0.6.0 (11/14/2018)
============
Expand Down
36 changes: 24 additions & 12 deletions src/main/java/hudson/plugins/ansicolor/ColorConsoleAnnotator.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,10 @@ final class ColorConsoleAnnotator extends ConsoleAnnotator<Object> {
private static final long serialVersionUID = 1;

private final String colorMapName;
private final String charset;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this could cause issues if someone upgraded the plugin to the version with this fix and then downgraded to 0.6.0, but it's not clear to me what the lifecycle of a serialized ConsoleAnnotator would be to know for sure if it would matter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is only serialized while a console page for a running build is being held open, so I do not think you need to care.


ColorConsoleAnnotator(String colorMapName, String charset) {
ColorConsoleAnnotator(String colorMapName) {
this.colorMapName = colorMapName;
this.charset = charset;
LOGGER.fine("creating annotator with colorMapName=" + colorMapName + " charset=" + charset);
LOGGER.fine("creating annotator with colorMapName=" + colorMapName);
}

@Override
Expand All @@ -70,12 +68,12 @@ class EmitterImpl implements AnsiAttributeElement.Emitter {
int lastPoint = -1; // multiple HTML tags may be emitted for one control sequence
@Override
public void emitHtml(String html) {
LOGGER.finest("emitting " + html + " @" + incoming.getCount());
LOGGER.log(Level.FINEST, "emitting {0} @{1}/{2}", new Object[] { html, incoming.getCount(), s.length() });
text.addMarkup(incoming.getCount(), html);
if (incoming.getCount() != lastPoint) {
lastPoint = incoming.getCount();
int hide = incoming.getCount() - outgoing.getCount() - adjustment;
LOGGER.finest("hiding " + hide + " @" + (outgoing.getCount() + adjustment));
LOGGER.log(Level.FINEST, "hiding {0} @{1}", new Object[] { hide, outgoing.getCount() + adjustment });
text.addMarkup(outgoing.getCount() + adjustment, outgoing.getCount() + adjustment + hide, "<!--", "-->");
adjustment += hide;
}
Expand All @@ -85,10 +83,24 @@ public void emitHtml(String html) {
CountingOutputStream incoming = new CountingOutputStream(new AnsiHtmlOutputStream(outgoing, colorMap, emitter));
emitter.incoming = incoming;
try {
byte[] data = s.getBytes(charset);
for (int i = 0; i < data.length; i++) {
// Do not use write(byte[]) as offsets in incoming would not be accurate.
incoming.write(data[i]);
/*
* We only use AnsiHtmlOutputStream for its calls to Emitter.emitHtml when it encounters ANSI escape
* sequences; the output of the stream will be discarded. To know where to insert HTML in the MarkupText,
* we track the number of bytes we have written, and use that as a char (UTF-16 code unit) offset into
* the original String. Since all ANSI escape sequences only use ASCII characters, and ASCII characters
* in UTF-16BE are all represented using a single code unit whose high byte is 0, and whose low byte is
* the same as it would be in an 8-bit ASCII encoding, we write all ASCII chars to the stream as the low
* byte of the code unit, and convert any other character into '?' as a placeholder so the number of
* bytes written matches the char offset into the String.
*/
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// The highest ASCII character is 0x7F (DEL). High and low surrogate pairs in UTF-16BE will always
// be at least 0xD800 and will be converted to '?'.
if (c >= 0x80) {
c = '?';
}
incoming.write(c);
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, null, x);
Expand All @@ -107,7 +119,7 @@ public ConsoleAnnotator<Object> newInstance(Object context) {
if (context instanceof Run) {
ColorizedAction action = ((Run) context).getAction(ColorizedAction.class);
if (action != null) {
return new ColorConsoleAnnotator(action.colorMapName, ((Run) context).getCharset().name());
return new ColorConsoleAnnotator(action.colorMapName);
}
} else if (Jenkins.get().getPlugin("workflow-api") != null && context instanceof FlowNode) {
FlowNode node = (FlowNode) context;
Expand All @@ -122,7 +134,7 @@ public ConsoleAnnotator<Object> newInstance(Object context) {
if (exec instanceof Run) {
ColorizedAction action = ((Run) exec).getAction(ColorizedAction.class);
if (action != null) {
return new ColorConsoleAnnotator(action.colorMapName, /* JEp-206 */ "UTF-8");
return new ColorConsoleAnnotator(action.colorMapName);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,32 @@ public void evaluate() throws Throwable {
}
});
}

@Test
public void testNonAscii() throws Exception {
story.then(r -> {
FreeStyleProject p = r.createFreeStyleProject();
p.getBuildWrappersList().add(new AnsiColorBuildWrapper(null));
p.getBuildersList().add(new TestBuilder() {
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException {
listener.getLogger().println("\033[94;1m[ INFO ] Récupération du numéro de version de l'application\033[0m");
listener.getLogger().println("\033[94;1m[ INFO ] ビルドのコンソール出力を取得します。\033[0m");
// There are 3 smiley face emojis in this String
listener.getLogger().println("\033[94;1m[ INFO ] 😀😀\033[0m😀");
return true;
}
});
FreeStyleBuild b = r.buildAndAssertSuccess(p);
StringWriter writer = new StringWriter();
b.getLogText().writeHtmlTo(0L, writer);
String html = writer.toString();
System.out.print(html);
assertThat(html.replaceAll("<!--.+?-->", ""),
allOf(
containsString("<span style=\"color: #4682B4;\"><b>[ INFO ] Récupération du numéro de version de l'application</b></span>"),
containsString("<span style=\"color: #4682B4;\"><b>[ INFO ] ビルドのコンソール出力を取得します。</b></span>"),
containsString("<span style=\"color: #4682B4;\"><b>[ INFO ] 😀😀</b></span>😀")));
});
}
}