[552542] Escape special characters in Strings rendered as HTML

- add code to escape special characters in Strings rendered as HTML,
including thread and class names  
- add some tests to verify the validity of the generated HTML

Change-Id: I35614d50182a21a757b3d626803a926898595bfc
diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
index d4136d9..9c2085e 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/LeakHunterQuery.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2018 SAP AG, IBM Corporation and others.

+ * Copyright (c) 2008, 2019 SAP AG, IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -71,6 +71,7 @@
 import org.eclipse.mat.snapshot.query.PieFactory;

 import org.eclipse.mat.snapshot.query.SnapshotQuery;

 import org.eclipse.mat.snapshot.registry.TroubleTicketResolverRegistry;

+import org.eclipse.mat.util.HTMLUtils;
 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.mat.util.MessageUtil;

 

@@ -243,7 +244,7 @@
         {

             overview.append("<p>"); //$NON-NLS-1$

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Thread, //

-                            suspect.getSuspect().getDisplayName(), //

+                            HTMLUtils.escapeText(suspect.getSuspect().getDisplayName()), //
                             formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

             overview.append("</p>"); //$NON-NLS-1$

         }

@@ -270,7 +271,7 @@
             String classloaderName = getClassLoaderName(suspectClassloader, keywords);

 

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Class, //

-                            className, classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

+                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));
         }

         else

         {

@@ -285,7 +286,7 @@
             String classloaderName = getClassLoaderName(suspectClassloader, keywords);

 

             overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_Instance, //

-                            className, classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

+                            HTMLUtils.escapeText(className), classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));
 

             /*

              * if the class name matches the skip pattern, try to find the first

@@ -326,8 +327,8 @@
 //                        involvedClassloaders.add(suspectClassloader);

                         objectsForTroubleTicketInfo.add(referrer);

                         String referrerClassloaderName = getClassLoaderName(referrerClassloader, keywords);

-                        overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedByInstance, referrer

-                                        .getDisplayName(), referrerClassloaderName));

+                        overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_ReferencedByInstance, HTMLUtils.escapeText(referrer
+                                        .getDisplayName()), referrerClassloaderName));
 

                     }

                 }

@@ -359,7 +360,7 @@
 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

 

-                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByLoadedBy, clazz.getName(),

+                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByLoadedBy, HTMLUtils.escapeText(clazz.getName()),
                                 classloaderName));

             }

             else

@@ -373,7 +374,7 @@
                 objectsForTroubleTicketInfo.add(accumulationObject);

 

                 String classloaderName = getClassLoaderName(accPointClassloader, keywords);

-                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByInstance, className,

+                overview.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_AccumulatedByInstance, HTMLUtils.escapeText(className),
                                 classloaderName));

             }

         }

@@ -466,7 +467,7 @@
         String classloaderName = getClassLoaderName(classloader, keywords);

 

         String numberOfInstances = numberFormatter.format(suspect.getSuspectInstances().length);

-        builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_InstancesOccupy, numberOfInstances, className,

+        builder.append(MessageUtil.format(Messages.LeakHunterQuery_Msg_InstancesOccupy, numberOfInstances, HTMLUtils.escapeText(className),
                         classloaderName, formatRetainedHeap(suspect.getSuspectRetained(), totalHeap)));

 

         int[] suspectInstances = suspect.getSuspectInstances();

@@ -485,12 +486,13 @@
             builder.append("<ul>"); //$NON-NLS-1$

             for (IObject inst : bigSuspectInstances)

             {

-                builder.append("<li>").append(inst.getDisplayName()); //$NON-NLS-1$

+                builder.append("<li>").append(HTMLUtils.escapeText(inst.getDisplayName())); //$NON-NLS-1$
                 builder.append("&nbsp;-&nbsp;") //$NON-NLS-1$

                                 .append(

                                                 MessageUtil.format(Messages.LeakHunterQuery_Msg_Bytes,

                                                                 formatRetainedHeap(inst.getRetainedHeapSize(),

                                                                                 totalHeap)));

+                builder.append("</li>"); //$NON-NLS-1$
             }

             builder.append("</ul>"); //$NON-NLS-1$

         }

@@ -643,6 +645,12 @@
         return name;

     }

 

+    /**
+     * Get the name of the class loader.
+     * @param classloader
+     * @param keywords
+     * @return The name with HTML escapes already applied.
+     */
     private String getClassLoaderName(IObject classloader, Set<String> keywords)

     {

         if (classloader.getObjectAddress() == 0)

@@ -656,7 +664,7 @@
             {

                 keywords.add(classloaderName);

             }

-            return classloaderName;

+            return HTMLUtils.escapeText(classloaderName);
         }

     }

 

@@ -754,7 +762,7 @@
     {

         builder.append("<b>").append(Messages.LeakHunterQuery_Keywords).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$

         for (String s : keywords)

-            builder.append(s).append("<br>"); //$NON-NLS-1$

+            builder.append(HTMLUtils.escapeText(s)).append("<br>"); //$NON-NLS-1$
     }

 

     private void appendTroubleTicketInformation(List<IObject> classloaders, StringBuilder builder)

@@ -766,12 +774,12 @@
 

             if (!mapping.isEmpty())

             {

-                builder.append("<br><b>").append(resolver.getTicketSystem()).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$

+                builder.append("<br><b>").append(HTMLUtils.escapeText(resolver.getTicketSystem())).append("</b><br>"); //$NON-NLS-1$ //$NON-NLS-2$
                 for (Map.Entry<String, String> entry : mapping.entrySet())

                 {

                     builder.append(

-                                    MessageUtil.format(Messages.LeakHunterQuery_TicketForSuspect, entry.getKey(), entry

-                                                    .getValue())).append("<br>"); //$NON-NLS-1$

+                                    MessageUtil.format(Messages.LeakHunterQuery_TicketForSuspect, HTMLUtils.escapeText(entry.getKey()), HTMLUtils.escapeText(entry
+                                                    .getValue()))).append("<br>"); //$NON-NLS-1$
                 }

             }

         }

@@ -798,7 +806,7 @@
                 builder.append("<p>"); //$NON-NLS-1$

 

                 for (CompositeResult.Entry requestInfo : requestInfos.getResultEntries())

-                    builder.append(requestInfo.getName()).append(" ").append( //$NON-NLS-1$

+                    builder.append(HTMLUtils.escapeText(requestInfo.getName())).append(" ").append( //$NON-NLS-1$
                                                     textResult.linkTo(Messages.LeakHunterQuery_RequestDetails,

                                                                     requestInfo.getResult())).append("<br>"); //$NON-NLS-1$

 

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/component/ComponentReportQuery.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/component/ComponentReportQuery.java
index f57448b..f816de7 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/component/ComponentReportQuery.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/component/ComponentReportQuery.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2018 SAP AG, IBM Corporation and others

+ * Copyright (c) 2008, 2019 SAP AG, IBM Corporation and others
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -59,6 +59,7 @@
 import org.eclipse.mat.snapshot.query.PieFactory;

 import org.eclipse.mat.snapshot.query.RetainedSizeDerivedData;

 import org.eclipse.mat.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.HTMLUtils;
 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.mat.util.MessageUtil;

 import org.eclipse.mat.util.Units;

@@ -379,8 +380,8 @@
 

             StringBuilder comment = new StringBuilder();

             comment.append(MessageUtil.format(Messages.ComponentReportQuery_Msg_FoundOccurrences, table.getRowCount(),

-                            totals.getLabel(2)));

-            comment.append("<p>" + Messages.ComponentReportQuery_TopElementsInclude + "</p><ul>"); //$NON-NLS-1$ //$NON-NLS-2$

+                            HTMLUtils.escapeText(totals.getLabel(2))));
+            comment.append("<p>").append(Messages.ComponentReportQuery_TopElementsInclude).append("</p><ul>"); //$NON-NLS-1$ //$NON-NLS-2$
 

             for (int rowId = 0; rowId < table.getRowCount() && rowId < 5; rowId++)

             {

@@ -392,7 +393,7 @@
                 String size = table.getFormattedColumnValue(row, 3);

 

                 comment.append("<li>").append(table.getFormattedColumnValue(row, 1)); //$NON-NLS-1$

-                comment.append(" &times; <strong>").append(value).append("</strong> "); //$NON-NLS-1$ //$NON-NLS-2$

+                comment.append(" &times; <strong>").append(HTMLUtils.escapeText(value)).append("</strong> "); //$NON-NLS-1$ //$NON-NLS-2$
                 comment.append(MessageUtil.format(Messages.ComponentReportQuery_Label_Bytes, size)).append("</li>"); //$NON-NLS-1$

             }

             comment.append("</ul>"); //$NON-NLS-1$

@@ -470,7 +471,7 @@
 

                             comment.append("<li>"); //$NON-NLS-1$

                             comment.append(MessageUtil.format(Messages.ComponentReportQuery_Msg_InstancesRetainBytes,

-                                            numberOfObjects, clazz.getName(), retainedSize));

+                                            numberOfObjects, HTMLUtils.escapeText(clazz.getName()), retainedSize));
                             comment.append("</li>"); //$NON-NLS-1$

                         }

 

@@ -557,7 +558,7 @@
 

                             comment.append("<li>"); //$NON-NLS-1$

                             comment.append(MessageUtil.format(Messages.ComponentReportQuery_Msg_InstancesRetainBytes,

-                                            numberOfObjects, clazz.getName(), retainedSize));

+                                            numberOfObjects, HTMLUtils.escapeText(clazz.getName()), retainedSize));
                             comment.append("</li>"); //$NON-NLS-1$

                         }

 

@@ -647,7 +648,7 @@
 

                         comment.append("<li>"); //$NON-NLS-1$

                         comment.append(MessageUtil.format(Messages.ComponentReportQuery_Msg_InstancesRetainBytes,

-                                        numberOfObjects, clazz.getName(), retainedSize));

+                                        numberOfObjects, HTMLUtils.escapeText(clazz.getName()), retainedSize));
                         comment.append("</li>"); //$NON-NLS-1$

                         break;

                     }

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/jetty/JettyRequestResolver.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/jetty/JettyRequestResolver.java
index 7548c06..a2898b4 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/jetty/JettyRequestResolver.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/inspections/jetty/JettyRequestResolver.java
@@ -22,6 +22,7 @@
 import org.eclipse.mat.snapshot.extension.Subject;

 import org.eclipse.mat.snapshot.model.IObject;

 import org.eclipse.mat.snapshot.query.SnapshotQuery;

+import org.eclipse.mat.util.HTMLUtils;
 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.mat.util.MessageUtil;

 

@@ -42,8 +43,8 @@
 

         // Summary

         StringBuilder buf = new StringBuilder(256);

-        buf.append(MessageUtil.format(Messages.JettyRequestResolver_Msg_ThreadExecutesHTTPRequest, requestURI

-                        .getClassSpecificName()));

+        buf.append(MessageUtil.format(Messages.JettyRequestResolver_Msg_ThreadExecutesHTTPRequest, HTMLUtils.escapeText(requestURI
+                        .getClassSpecificName())));
         String summary = buf.toString();

         QuerySpec spec = new QuerySpec(Messages.JettyRequestResolver_Summary);

         spec.setCommand("list_objects 0x" + Long.toHexString(httpRequest.getObjectAddress())); //$NON-NLS-1$

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
index aba5eb4..72385f1 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/internal/messages.properties
@@ -141,7 +141,7 @@
 ComponentReportQuery_Msg_NoCollisionRatiosFound=No maps found with collision ratios greater than 80%.

 ComponentReportQuery_Msg_NoExcessiveEmptyCollectionsFound=No excessive usage of empty collections found.

 ComponentReportQuery_Msg_NoFinalizerFound=Component does not keep object with Finalizer methods alive.

-ComponentReportQuery_Msg_NoFinalizerObjects=Heap dump contains no java.lang.ref.Finalizer objects.<br/>IBM VMs implement Finalizer differently and are currently not supported by this report.

+ComponentReportQuery_Msg_NoFinalizerObjects=Heap dump contains no java.lang.ref.Finalizer objects.<br>IBM VMs implement Finalizer differently and are currently not supported by this report.
 ComponentReportQuery_Msg_NoLowFillRatiosFound=No serious amount of collections with low fill ratios found.

 ComponentReportQuery_Msg_NoSoftReferencesFound=Heap dump contains no soft references.

 ComponentReportQuery_Msg_NoWeakReferencesFound=Heap dump contains no weak references.

diff --git a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/snapshot/query/PieFactory.java b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/snapshot/query/PieFactory.java
index 658f4b2..1d27e83 100644
--- a/plugins/org.eclipse.mat.api/src/org/eclipse/mat/snapshot/query/PieFactory.java
+++ b/plugins/org.eclipse.mat.api/src/org/eclipse/mat/snapshot/query/PieFactory.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2012 SAP AG and others.

+ * Copyright (c) 2008, 2019 SAP AG and others.
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -258,7 +258,7 @@
     private final static class SliceImpl implements IResultPie.ColoredSlice, Serializable

     {

         private static final long serialVersionUID = 1L;

-        private static final String HTML_BREAK = "<br/>"; //$NON-NLS-1$

+        private static final String HTML_BREAK = "<br>"; //$NON-NLS-1$
 

         int objectId;

 

diff --git a/plugins/org.eclipse.mat.chart/src/org/eclipse/mat/impl/chart/HtmlPieChartRenderer.java b/plugins/org.eclipse.mat.chart/src/org/eclipse/mat/impl/chart/HtmlPieChartRenderer.java
index fd5d4a7..0007ead 100644
--- a/plugins/org.eclipse.mat.chart/src/org/eclipse/mat/impl/chart/HtmlPieChartRenderer.java
+++ b/plugins/org.eclipse.mat.chart/src/org/eclipse/mat/impl/chart/HtmlPieChartRenderer.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2008, 2011 SAP AG and others.

+ * Copyright (c) 2008, 2019 SAP AG and others.
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -48,6 +48,7 @@
 import org.eclipse.mat.query.IResultPie;

 import org.eclipse.mat.report.IOutputter;

 import org.eclipse.mat.report.Renderer;

+import org.eclipse.mat.util.HTMLUtils;
 

 @Renderer(target = "html", result = IResultPie.class)

 public class HtmlPieChartRenderer implements IOutputter

@@ -141,10 +142,10 @@
     {

         StringBuilder message = new StringBuilder();

         message.append(Messages.HtmlPieChartRenderer_ErrorRenderingChart);

-        message.append(e.getClass().getName());

+        message.append(HTMLUtils.escapeText(e.getClass().getName()));
 

         if (e.getMessage() != null)

-            message.append(": ").append(e.getMessage()); //$NON-NLS-1$

+            message.append(": ").append(HTMLUtils.escapeText(e.getMessage())); //$NON-NLS-1$
 

         String msg = message.toString();

 

diff --git a/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/JRubyScriptResolver.java b/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/JRubyScriptResolver.java
index 4ee3f71..c926f1b 100644
--- a/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/JRubyScriptResolver.java
+++ b/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/JRubyScriptResolver.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2010 SAP AG.

+ * Copyright (c) 2010, 2019 SAP AG and IBM Corporation.
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -7,6 +7,7 @@
  *

  * Contributors:

  *    Dimitar Giormov - initial API and implementation

+ *    Andrew Johnson (IBM Corporation) - escapes
  *******************************************************************************/

 package org.eclipse.mat.jruby.resolver;

 

@@ -25,6 +26,7 @@
 import org.eclipse.mat.snapshot.model.IClass;

 import org.eclipse.mat.snapshot.model.IObject;

 import org.eclipse.mat.snapshot.model.NamedReference;

+import org.eclipse.mat.util.HTMLUtils;
 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.osgi.util.NLS;

 

@@ -79,22 +81,22 @@
 		}

 		if (classSpecificName.length() > 0){

 			String fileName = new File(classSpecificName).getName();

-			String summary = NLS.bind(Messages.JRubyScriptResolver_Summary, fileName);

+			String summary = NLS.bind(Messages.JRubyScriptResolver_Summary, HTMLUtils.escapeText(fileName));
 			

 			String rubyCallMessage = (shortJavaName == null)

-					? NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyCall_Class, fileName)

-					: NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyCall_Method, fileName, realClassName);

+					? NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyCall_Class, HTMLUtils.escapeText(fileName))
+					: NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyCall_Method, HTMLUtils.escapeText(fileName), HTMLUtils.escapeText(realClassName));
 			

 			String possibleBundleSuspectsMessage = ""; //$NON-NLS-1$

 			if (isOsgiBased && possibleBundleSuspects.size() > 0){

 				thread.addKeyword("osgi"); //$NON-NLS-1$

 				StringBuilder suspects = new StringBuilder();

 				for (String possibleBundleSuspect : possibleBundleSuspects) {

-					suspects.append(possibleBundleSuspect).append(' ');

+					suspects.append(HTMLUtils.escapeText(possibleBundleSuspect)).append(' ');
 				}

 				possibleBundleSuspectsMessage = NLS.bind(Messages.JRubyScriptResolver_ResultBody_PossibleSuspects, suspects);

 			}

-			String rubyScriptPathMessage = NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyScriptPath, classSpecificName);

+			String rubyScriptPathMessage = NLS.bind(Messages.JRubyScriptResolver_ResultBody_RubyScriptPath, HTMLUtils.escapeText(classSpecificName));
 			

 			String resultBody = NLS.bind(Messages.JRubyScriptResolver_ResultBody, new Object[] { rubyCallMessage, possibleBundleSuspectsMessage, rubyScriptPathMessage });

 			result.addResult(Messages.JRubyScriptResolver_ResultHeader, new TextResult(resultBody, true));

diff --git a/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/RubyStacktraceDumper.java b/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/RubyStacktraceDumper.java
index 4a1a789..d92ec1d 100644
--- a/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/RubyStacktraceDumper.java
+++ b/plugins/org.eclipse.mat.jruby.resolver/src/org/eclipse/mat/jruby/resolver/RubyStacktraceDumper.java
@@ -1,5 +1,5 @@
 /*******************************************************************************

- * Copyright (c) 2010, 2012 SAP AG and IBM Corporation.

+ * Copyright (c) 2010, 2019 SAP AG and IBM Corporation.
  * All rights reserved. This program and the accompanying materials

  * are made available under the terms of the Eclipse Public License v1.0

  * which accompanies this distribution, and is available at

@@ -29,6 +29,7 @@
 import org.eclipse.mat.snapshot.model.IObject;

 import org.eclipse.mat.snapshot.model.IObjectArray;

 import org.eclipse.mat.snapshot.model.NamedReference;

+import org.eclipse.mat.util.HTMLUtils;
 import org.eclipse.mat.util.IProgressListener;

 import org.eclipse.osgi.util.NLS;

 

@@ -63,7 +64,7 @@
             

             StringBuilder stackTrace = new StringBuilder();

             for (PrintableStackFrame element : stackTraceFrames){

-                stackTrace.append(NLS.bind(Messages.RubyStacktraceDumper_StackTraceLine, element));

+                stackTrace.append(NLS.bind(Messages.RubyStacktraceDumper_StackTraceLine, HTMLUtils.escapeText(element.toString())));
             }

 

             String summary = NLS.bind(Messages.RubyStacktraceDumper_Summary, javaThreadName);

diff --git a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
index 3b4bdb4..6161298 100644
--- a/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
+++ b/plugins/org.eclipse.mat.report/src/org/eclipse/mat/report/internal/HtmlOutputter.java
@@ -96,6 +96,7 @@
     // //////////////////////////////////////////////////////////////

     /**

      * Extract alternate text for image URL.

+     * Use result within double quotes for attributes.
      * @param url

      * @return the text or an empty string

      */

@@ -103,7 +104,7 @@
     private String altText(URL url)

     {

         String alt = "";

-        return HTMLUtils.escapeText(alt);

+        return HTMLUtils.escapeText(alt).replace("\"","&quot;");
     }

 

     @SuppressWarnings("nls")

@@ -176,12 +177,12 @@
                 if (filter[i].isActive())

                     artefact.append("<td>").append(HTMLUtils.escapeText(filter[i].getCriteria())).append("</td>");

                 else

-                    artefact.append("<td/>");

+                    artefact.append("<td></td>");
             }

         }

 

         if (hasDetailsLink)

-            artefact.append("<td/>");

+            artefact.append("<td></td>");
 

         artefact.append("</tr>");

     }

@@ -249,7 +250,7 @@
             }

 

             if (hasDetailsLink)

-                artefact.append("<td/>");

+                artefact.append("<td></td>");
 

             artefact.append("</tr>");

         }

@@ -657,11 +658,10 @@
         }

         else

         {

-            writer.append("<p>");//$NON-NLS-1$

+            // <pre> is a block level tag so cannot be surrounded by <p>
             writer.append("<pre>"); //$NON-NLS-1$

             writer.append(HTMLUtils.escapeText(textResult.getText()));

             writer.append("</pre>"); //$NON-NLS-1$

-            writer.append("</p>");//$NON-NLS-1$

         }

     }

 

diff --git a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
index 396f88f..0947332 100644
--- a/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
+++ b/plugins/org.eclipse.mat.tests/src/org/eclipse/mat/tests/snapshot/GeneralSnapshotTests.java
@@ -11,15 +11,16 @@
  *******************************************************************************/

 package org.eclipse.mat.tests.snapshot;

 

+import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.containsString;

 import static org.hamcrest.CoreMatchers.either;

 import static org.hamcrest.CoreMatchers.equalTo;

 import static org.hamcrest.CoreMatchers.not;

-import static org.hamcrest.CoreMatchers.anyOf;

 import static org.hamcrest.Matchers.greaterThan;

 import static org.hamcrest.Matchers.greaterThanOrEqualTo;

 import static org.hamcrest.collection.IsEmptyCollection.emptyCollectionOf;

 import static org.hamcrest.core.IsNull.nullValue;

+import static org.hamcrest.core.IsInstanceOf.instanceOf;
 import static org.junit.Assert.assertEquals;

 import static org.junit.Assert.assertNotNull;

 import static org.junit.Assert.assertSame;

@@ -30,17 +31,26 @@
 import static org.junit.Assume.assumeTrue;

 

 import java.io.File;

+import java.io.FileInputStream;
 import java.io.IOException;

+import java.io.InputStreamReader;
 import java.io.Serializable;

+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;

 import java.util.Collection;

 import java.util.Collections;

+import java.util.HashSet;
+import java.util.Set;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 

 import org.eclipse.core.runtime.preferences.IEclipsePreferences;

 import org.eclipse.core.runtime.preferences.InstanceScope;

 import org.eclipse.mat.SnapshotException;

 import org.eclipse.mat.collect.SetInt;

 import org.eclipse.mat.query.IResult;

+import org.eclipse.mat.query.results.DisplayFileResult;
 import org.eclipse.mat.snapshot.ISnapshot;

 import org.eclipse.mat.snapshot.SnapshotFactory;

 import org.eclipse.mat.snapshot.SnapshotInfo;

@@ -324,7 +334,7 @@
         else if (hasMethods == Methods.FRAMES_ONLY)

         {

             assertEquals(1, methods);

-            assertTrue(methodsWithObjects > 0);

+            assertThat(methodsWithObjects, greaterThan(0));
         }

         else

         {

@@ -336,47 +346,257 @@
     @Test

     public void testClassLoaders() throws SnapshotException

     {

-        assertTrue(snapshot.getSnapshotInfo().getNumberOfClassLoaders() > 1);

+        assertThat(snapshot.getSnapshotInfo().getNumberOfClassLoaders(), greaterThan(1));
     }

 

     @Test

-    public void testRegressionReport() throws SnapshotException

+    public void testRegressionReport() throws SnapshotException, IOException
     {

         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.tests:regression", snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        checkHTMLResult(t);
     }

 

     @Test

-    public void testPerformanceReport() throws SnapshotException

+    public void testPerformanceReport() throws SnapshotException, IOException
     {

         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.tests:performance", snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        checkHTMLResult(t);
     }

 

     @Test

-    public void testLeakSuspectsReport() throws SnapshotException

+    public void testLeakSuspectsReport() throws SnapshotException, IOException
     {

         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:suspects", snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        checkHTMLResult(t);
     }

 

     @Test

-    public void testOverviewReport() throws SnapshotException

+    public void testOverviewReport() throws SnapshotException, IOException
     {

         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:overview", snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        checkHTMLResult(t);
     }

 

     @Test

-    public void testTopComponentsReport() throws SnapshotException

+    public void testTopComponentsReport() throws SnapshotException, IOException
     {

         SnapshotQuery query = SnapshotQuery.parse("default_report org.eclipse.mat.api:top_components", snapshot);

         IResult t = query.execute(new VoidProgressListener());

         assertNotNull(t);

+        checkHTMLResult(t);
+    }
+
+    public void checkHTMLResult(IResult r) throws IOException
+    {
+        assertThat(r, instanceOf(DisplayFileResult.class));
+        if (r instanceof DisplayFileResult)
+        {
+            DisplayFileResult d = (DisplayFileResult)r;
+            File f = d.getFile();
+            checkHTMLFile(f);
+        }
+    }
+
+    /**
+     * Recursively check an HTML file.
+     * @param f
+     * @throws IOException
+     */
+    public void checkHTMLFile(File f) throws IOException
+    {
+        Set<File>seen = new HashSet<File>();
+        checkHTMLFile(f, seen);
+    }
+
+    /**
+     * Recursively check an HTML file, avoiding going
+     * into files already seen.
+     * @param f
+     * @param seen Files already seen
+     * @throws IOException
+     */
+    public void checkHTMLFile(File f, Set<File>seen) throws IOException
+    {
+        // canonical needed to avoid problems with ..
+        if (!seen.add(f.getCanonicalFile()))
+            return;
+        FileInputStream fis = new FileInputStream(f);
+        try 
+        {
+            if (!f.getName().endsWith(".html"))
+            {
+                // Not HTML
+                return;
+            }
+            // Read the file into a string
+            InputStreamReader ir = new InputStreamReader(fis, StandardCharsets.UTF_8);
+            char cbuf[] = new char[(int)f.length()];
+            int l = ir.read(cbuf);
+            String s = new String(cbuf, 0, l);
+
+            /*
+             *  All these checks are approximate and would be confused
+             *  by false tags in attribute value string etc.
+             */
+
+            // Some basic checks
+            assertThat("Expected charset", s, containsString("content=\"text/html;charset=UTF-8\""));
+            assertThat("Possible double escaping <", s, not(containsString("&amp;lt;")));
+            assertThat("Possible double escaping &", s, not(containsString("&amp;amp;")));
+
+            /*
+             * Rough test for bad tag - might indicate unescaped '<'.
+             * Find a less-than sign
+             *  Negative lookahead for:
+             *   optional / or !
+             *   series of letters
+             *   then optional digits
+             *   ending with a space or greater-than
+             *   or !DOCTYPE
+             *  then match all until next space or greater-than
+             * We normally have lower case tags and no self-closed tags.
+             */
+            Pattern p = Pattern.compile("<(?!(/?[a-z]+[0-9]*)[ >]|!DOCTYPE )[^ >]*");
+            Matcher m = p.matcher(s);
+            String v;
+            if (m.find())
+            {
+                v = m.group(0);
+            }
+            else
+            {
+                v = null;
+            }
+            assertThat("Bad tag in "+f, v, equalTo(null));
+
+            /*
+             * Rough test for bad entity or unescaped ampersand.
+             * Negative lookahead for
+             * entity name followed by semicolon
+             * entity number preceded by # followed by semicolon 
+             */
+            p = Pattern.compile("&(?!([a-z]+;)|(#[0-9]+;))[^a-z#]+");
+            m = p.matcher(s);
+            if (m.find())
+            {
+                v = m.group(0);
+            }
+            else
+            {
+                v = null;
+            }
+            assertThat("Bad entity in "+f, v, equalTo(null));
+
+            /*
+             * Check for alt text for images.
+             */
+            p = Pattern.compile("<img (?![^>]*alt)[^>]*>");
+            m = p.matcher(s);
+            if (m.find())
+            {
+                v = m.group(0);
+            }
+            else
+            {
+                v = null;
+            }
+            assertThat("No alt for img in "+f, v, equalTo(null));
+
+            /*
+             * Rough check for nesting of tags.
+             */
+            Stack<String> stk = new Stack<String>();
+            Stack<Integer> stki = new Stack<Integer>();
+            // Matches tags
+            p = Pattern.compile("</?[a-z]+");
+            m = p.matcher(s);
+            while (m.find())
+            {
+                String tag = m.group().substring(1);
+                if (tag.startsWith("/"))
+                {
+                    // Closing tag
+                    assertThat("Stack for "+tag, stk.size(), greaterThan(0));
+                    tag = tag.substring(1);
+                    String tag2 = stk.pop();
+                    int si = stki.pop();
+                    if (tag2.equals("p") && !tag.equals("a") && !tag.equals("p"))
+                    {
+                        // <p> closed by any outer tag except <a>
+                        tag2 = stk.pop();
+                        si = stki.pop();
+                        assertThat("Stack for "+tag, stk.size(), greaterThan(0));
+                    }
+                    String range = s.substring(si, m.end());
+                    assertThat("Tag closing at " + m.start()+" "+range+" "+f, tag, equalTo(tag2));
+                }
+                else
+                {
+                    // Self closing tag?
+                    if (!(tag.equals("br") 
+                                    || tag.equals("hr")
+                                    || tag.equals("img")
+                                    || tag.equals("link")
+                                    || tag.equals("input")
+                                    || tag.equals("meta")
+                                    || tag.equals("area")))
+                    {
+                        // <p> is closed by following block tag
+                        if (stk.size() >= 1 && stk.peek().equals("p") && (
+                                        tag.equals("h1") ||
+                                        tag.equals("h2") ||
+                                        tag.equals("h3") ||
+                                        tag.equals("h4") ||
+                                        tag.equals("h5") ||
+                                        tag.equals("h6") ||
+                                        tag.equals("pre") ||
+                                        tag.equals("ol") ||
+                                        tag.equals("ul") ||
+                                        tag.equals("div")))
+                        {
+                            // Close the <p> tag
+                            stk.pop();
+                            stki.pop();
+                        }
+                        stk.push(tag);
+                        stki.push(m.start());
+                    }
+                }
+            }
+            assertThat("Stack should be empty", stk.size(), equalTo(0));
+
+            // Look for references to other files
+            for (int i = 0; i >= 0; )
+            {
+                String match = "href=\"";
+                i = s.indexOf(match, i);
+                if (i >= 0)
+                {
+                    int j = s.indexOf("\"", i + match.length());
+                    String fn = s.substring(i + match.length(), j);
+                    if (!fn.startsWith("/") && !fn.contains("#") && !fn.startsWith("http") && !fn.startsWith("mat:"))
+                    {
+                        File d = f.getParentFile();
+                        File newf = new File(d, fn);
+                        checkHTMLFile(newf, seen);
+                    }
+                    i = j;
+                }
+            }
+            ir.close();
+        }
+        finally
+        {
+            fis.close();
+        }
     }

 

     @Test

@@ -779,7 +999,7 @@
                     ISnapshot sn3 = SnapshotFactory.openSnapshot(newPath, new VoidProgressListener());

                     try

                     {

-                        assertTrue(sn3.getHeapSize(0) >= 0);

+                        assertThat(sn3.getHeapSize(0), greaterThanOrEqualTo(0L));
                         // Do a complex operation which requires the dump still

                         // to be open and alive

                         assertNotNull(sn3.getObject(sn2.getClassOf(0).getClassLoaderId()).getOutboundReferences());

@@ -788,7 +1008,7 @@
                     {

                         SnapshotFactory.dispose(sn3);

                     }

-                    assertTrue(sn2.getHeapSize(0) >= 0);

+                    assertThat(sn2.getHeapSize(0), greaterThanOrEqualTo(0L));
                     // Do a complex operation which requires the dump still to be open and alive.

                     assertNotNull(sn2.getObject(sn2.getClassOf(0).getClassLoaderId()).getOutboundReferences());

                 }