Unsafe public fields in UpdateInterface class: make public fields final

This commit makes the UpdateInterface class immutable. The Config -> UpdateInterface initialization is moved to the UpdateInterface constructor. Default values for the fields are moved to the constructor as well. Instances of String[] are changed to List<String> and made immutable. Instances of java.awt.Rectangle (which is mutable) are replaced with a new immutable com.threerings.getdown.util.Rectangle class.

Although java.awt.Color is (sort of) immutable, it was also removed and replaced with a new immutable com.threerings.getdown.util.Color class, since it was the last dependency from the getdown core module to anything in the java.awt package, which means that when we migrate to Java 9+ the core module should no longer require a dependency on the java.desktop module.

The assertions and expected values in ColorTest have been verified against the corresponding java.awt.Color values.

(Triggered by internal security audit and Fortify analysis.)
This commit is contained in:
Daniel Gredler
2018-09-05 17:07:58 -04:00
parent 31c8315c1f
commit 1f2c736564
9 changed files with 331 additions and 97 deletions
@@ -5,8 +5,6 @@
package com.threerings.getdown.data;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.*;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
@@ -57,7 +55,7 @@ public class Application
public static final String MANIFEST_CLASS = "manifest";
/** Used to communicate information about the UI displayed when updating the application. */
public static class UpdateInterface
public static final class UpdateInterface
{
/**
* The major steps involved in updating, along with some arbitrary percentages
@@ -85,68 +83,67 @@ public class Application
}
/** The human readable name of this application. */
public String name;
public final String name;
/** A background color, just in case. */
public Color background = Color.white;
public final Color background;
/** Background image specifiers for `RotatingBackgrounds`. */
public String[] rotatingBackgrounds;
public final List<String> rotatingBackgrounds;
/** The error background image for `RotatingBackgrounds`. */
public String errorBackground;
public final String errorBackground;
/** The paths (relative to the appdir) of images for the window icon. */
public String[] iconImages;
public final List<String> iconImages;
/** The path (relative to the appdir) to a single background image. */
public String backgroundImage;
public final String backgroundImage;
/** The path (relative to the appdir) to the progress bar image. */
public String progressImage;
public final String progressImage;
/** The dimensions of the progress bar. */
public Rectangle progress = new Rectangle(5, 5, 300, 15);
public final Rectangle progress;
/** The color of the progress text. */
public Color progressText = Color.black;
public final Color progressText;
/** The color of the progress bar. */
public Color progressBar = new Color(0x6699CC);
public final Color progressBar;
/** The dimensions of the status display. */
public Rectangle status = new Rectangle(5, 25, 500, 100);
public final Rectangle status;
/** The color of the status text. */
public Color statusText = Color.black;
public final Color statusText;
/** The color of the text shadow. */
public Color textShadow;
public final Color textShadow;
/** Where to point the user for help with install errors. */
public String installError;
public final String installError;
/** The dimensions of the patch notes button. */
public Rectangle patchNotes = new Rectangle(5, 50, 112, 26);
public final Rectangle patchNotes;
/** The patch notes URL. */
public String patchNotesUrl;
public final String patchNotesUrl;
/** Whether window decorations are hidden for the UI. */
public boolean hideDecorations;
public final boolean hideDecorations;
/** Whether progress text should be hidden or not. */
public boolean hideProgressText;
public final boolean hideProgressText;
/** The minimum number of seconds to display the GUI. This is to prevent the GUI from
* flashing up on the screen and immediately disappearing, which can be confusing to the
* user. */
public int minShowSeconds = 5;
public final int minShowSeconds;
/** The global percentages for each step. A step may have more than one, and
* the lowest reasonable one is used if a step is revisited. */
public Map<Step, List<Integer>> stepPercentages =
new EnumMap<Step, List<Integer>>(Step.class);
public final Map<Step, List<Integer>> stepPercentages;
/** Generates a string representation of this instance. */
@Override
@@ -160,11 +157,56 @@ public class Application
", hideProgressText" + hideProgressText + ", minShow=" + minShowSeconds + "]";
}
/** Initializer */
public UpdateInterface (Config config)
{
this.name = config.getString("ui.name");
this.progress = config.getRect("ui.progress", new Rectangle(5, 5, 300, 15));
this.progressText = config.getColor("ui.progress_text", Color.BLACK);
this.hideProgressText = config.getBoolean("ui.hide_progress_text");
this.minShowSeconds = config.getInt("ui.min_show_seconds", 5);
this.progressBar = config.getColor("ui.progress_bar", new Color(0x66, 0x99, 0xCC));
this.status = config.getRect("ui.status", new Rectangle(5, 25, 500, 100));
this.statusText = config.getColor("ui.status_text", Color.BLACK);
this.textShadow = config.getColor("ui.text_shadow", null);
this.hideDecorations = config.getBoolean("ui.hide_decorations");
this.backgroundImage = config.getString("ui.background_image",
config.getString("ui.background")); // support legacy format
// and now ui.background can refer to the background color, but fall back to black
// or white, depending on the brightness of the progressText
Color defaultBackground = (.5f < this.progressText.brightness())
? Color.BLACK
: Color.WHITE;
this.background = config.getColor("ui.background", defaultBackground);
this.progressImage = config.getString("ui.progress_image");
this.rotatingBackgrounds = stringsToList(config.getMultiValue("ui.rotating_background"));
this.iconImages = stringsToList(config.getMultiValue("ui.icon"));
this.errorBackground = config.getString("ui.error_background");
// On an installation error, where do we point the user.
String installError = config.getUrl("ui.install_error", null);
this.installError = (installError == null) ?
"m.default_install_error" : MessageUtil.taint(installError);
// the patch notes bits
this.patchNotes = config.getRect("ui.patch_notes", new Rectangle(5, 50, 112, 26));
this.patchNotesUrl = config.getUrl("ui.patch_notes_url", null);
// step progress percentage (defaults and then customized values)
EnumMap<Step, List<Integer>> stepPercentages = new EnumMap<Step, List<Integer>>(Step.class);
for (Step step : Step.values()) {
stepPercentages.put(step, step.defaultPercents);
}
for (UpdateInterface.Step step : UpdateInterface.Step.values()) {
String spec = config.getString("ui.percents." + step.name());
if (spec != null) {
try {
stepPercentages.put(step, intsToList(StringUtil.parseIntArray(spec)));
} catch (Exception e) {
log.warning("Failed to parse percentages for " + step + ": " + spec);
}
}
}
this.stepPercentages = Collections.unmodifiableMap(stepPercentages);
}
}
@@ -696,59 +738,13 @@ public class Application
_codeCacheRetentionDays = config.getInt("code_cache_retention_days", 7);
// parse and return our application config
UpdateInterface ui = new UpdateInterface();
_name = ui.name = config.getString("ui.name");
ui.progress = config.getRect("ui.progress", ui.progress);
ui.progressText = config.getColor("ui.progress_text", ui.progressText);
ui.hideProgressText = config.getBoolean("ui.hide_progress_text");
ui.minShowSeconds = config.getInt("ui.min_show_seconds", ui.minShowSeconds);
ui.progressBar = config.getColor("ui.progress_bar", ui.progressBar);
ui.status = config.getRect("ui.status", ui.status);
ui.statusText = config.getColor("ui.status_text", ui.statusText);
ui.textShadow = config.getColor("ui.text_shadow", ui.textShadow);
ui.hideDecorations = config.getBoolean("ui.hide_decorations");
ui.backgroundImage = config.getString("ui.background_image");
if (ui.backgroundImage == null) { // support legacy format
ui.backgroundImage = config.getString("ui.background");
}
// and now ui.background can refer to the background color, but fall back to black
// or white, depending on the brightness of the progressText
Color defaultBackground = (.5f < Color.RGBtoHSB(
ui.progressText.getRed(), ui.progressText.getGreen(), ui.progressText.getBlue(),
null)[2])
? Color.BLACK
: Color.WHITE;
ui.background = config.getColor("ui.background", defaultBackground);
ui.progressImage = config.getString("ui.progress_image");
ui.rotatingBackgrounds = config.getMultiValue("ui.rotating_background");
ui.iconImages = config.getMultiValue("ui.icon");
ui.errorBackground = config.getString("ui.error_background");
UpdateInterface ui = new UpdateInterface(config);
_name = ui.name;
_dockIconPath = config.getString("ui.mac_dock_icon");
if (_dockIconPath == null) {
_dockIconPath = "../desktop.icns"; // use a sensible default
}
// On an installation error, where do we point the user.
String installError = config.getUrl("ui.install_error", null);
ui.installError = (installError == null) ?
"m.default_install_error" : MessageUtil.taint(installError);
// the patch notes bits
ui.patchNotes = config.getRect("ui.patch_notes", ui.patchNotes);
ui.patchNotesUrl = config.getUrl("ui.patch_notes_url", null);
// step progress percentages
for (UpdateInterface.Step step : UpdateInterface.Step.values()) {
String spec = config.getString("ui.percents." + step.name());
if (spec != null) {
try {
ui.stepPercentages.put(step, intsToList(StringUtil.parseIntArray(spec)));
} catch (Exception e) {
log.warning("Failed to parse percentages for " + step + ": " + spec);
}
}
}
return ui;
}
@@ -1639,6 +1635,18 @@ public class Application
return Collections.unmodifiableList(list);
}
/**
* Make an immutable List from the specified String array.
*/
public static List<String> stringsToList (String[] values)
{
List<String> list = new ArrayList<>(values.length);
for (String val : values) {
list.add(val);
}
return Collections.unmodifiableList(list);
}
/** Used to parse resources with the specified name. */
protected void parseResources (Config config, String name, EnumSet<Resource.Attr> attrs,
List<Resource> list)
@@ -0,0 +1,81 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
/**
* An immutable color.
*/
public class Color
{
public final static Color WHITE = new Color(255, 255, 255);
public final static Color BLACK = new Color(0, 0, 0);
public final int red;
public final int green;
public final int blue;
public final int alpha;
public Color (int r, int g, int b)
{
this.red = Math.min(Math.max(r, 0), 255);
this.green = Math.min(Math.max(g, 0), 255);
this.blue = Math.min(Math.max(b, 0), 255);
this.alpha = 255;
}
public Color (int rgba, boolean hasAlpha)
{
this.red = (rgba >> 16) & 0xFF;
this.green = (rgba >> 8) & 0xFF;
this.blue = (rgba >> 0) & 0xFF;
if (hasAlpha) {
this.alpha = (rgba >> 24) & 0xFF;
} else {
this.alpha = 255;
}
}
/**
* Returns the combined RBGA value for this color.
*/
public int rgba ()
{
return ((alpha & 0xFF) << 24) |
((red & 0xFF) << 16) |
((green & 0xFF) << 8) |
((blue & 0xFF) << 0);
}
/**
* Returns the brightness of this color, per the standard HSB model.
*/
public float brightness ()
{
int max = Math.max(Math.max(red, green), blue);
return ((float) max) / 255.0f;
}
/** {@inheritDoc} */
@Override
public boolean equals(Object other)
{
return other instanceof Color && ((Color)other).rgba() == this.rgba();
}
/** {@inheritDoc} */
@Override
public int hashCode()
{
return rgba();
}
/** {@inheritDoc} */
public String toString ()
{
return getClass().getName() + "[r=" + red + ", g=" + green + ", b=" + blue + ", a=" + alpha + "]";
}
}
@@ -5,9 +5,6 @@
package com.threerings.getdown.util;
import java.awt.Color;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -32,6 +29,9 @@ import static com.threerings.getdown.Log.log;
*/
public class Config
{
/** Empty configuration. */
public static final Config EMPTY = new Config(new HashMap<String, Object>());
/** Options that control the {@link #parsePairs} function. */
public static class ParseOpts {
// these should be tweaked as desired by the caller
@@ -0,0 +1,31 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
/**
* An immutable rectangle.
*/
public class Rectangle
{
public final int x;
public final int y;
public final int width;
public final int height;
public Rectangle (int x, int y, int width, int height)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/** {@inheritDoc} */
public String toString ()
{
return getClass().getName() + "[x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "]";
}
}
@@ -0,0 +1,111 @@
//
// Getdown - application installer, patcher and launcher
// Copyright (C) 2004-2016 Getdown authors
// https://github.com/threerings/getdown/blob/master/LICENSE
package com.threerings.getdown.util;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
/**
* Tests {@link Color}.
*/
public class ColorTest
{
@Test
public void testRgbConstructor ()
{
Color color1 = new Color(0, 0, 0);
assertEquals(0, color1.red);
assertEquals(0, color1.green);
assertEquals(0, color1.blue);
assertEquals(255, color1.alpha);
assertEquals(0xFF000000, color1.rgba());
assertEquals(0, color1.brightness(), 0.0000001);
assertEquals(Color.BLACK, color1);
Color color2 = new Color(255, 255, 255);
assertEquals(255, color2.red);
assertEquals(255, color2.green);
assertEquals(255, color2.blue);
assertEquals(255, color2.alpha);
assertEquals(0xFFFFFFFF, color2.rgba());
assertEquals(1, color2.brightness(), 0.0000001);
assertEquals(Color.WHITE, color2);
Color color3 = new Color(1, 2, 3);
assertEquals(1, color3.red);
assertEquals(2, color3.green);
assertEquals(3, color3.blue);
assertEquals(255, color3.alpha);
assertEquals(0xFF010203, color3.rgba());
assertEquals(0.0117647, color3.brightness(), 0.0000001);
Color color4 = new Color(-100, 300, 200);
assertEquals(0, color4.red);
assertEquals(255, color4.green);
assertEquals(200, color4.blue);
assertEquals(255, color4.alpha);
assertEquals(0xFF00FFC8, color4.rgba());
assertEquals(1, color4.brightness(), 0.0000001);
}
@Test
public void testRgbaConstructor ()
{
Color color1 = new Color(0, false);
assertEquals(0, color1.red);
assertEquals(0, color1.green);
assertEquals(0, color1.blue);
assertEquals(255, color1.alpha);
assertEquals(0xFF000000, color1.rgba());
assertEquals(0, color1.brightness(), 0.0000001);
assertEquals(Color.BLACK, color1);
Color color2 = new Color(0, true);
assertEquals(0, color2.red);
assertEquals(0, color2.green);
assertEquals(0, color2.blue);
assertEquals(0, color2.alpha);
assertEquals(0, color2.rgba());
assertEquals(0, color2.brightness(), 0.0000001);
assertFalse(Color.BLACK.equals(color2));
Color color3 = new Color(0x00FFFFFF, false);
assertEquals(255, color3.red);
assertEquals(255, color3.green);
assertEquals(255, color3.blue);
assertEquals(255, color3.alpha);
assertEquals(0xFFFFFFFF, color3.rgba());
assertEquals(1, color3.brightness(), 0.0000001);
assertEquals(Color.WHITE, color3);
Color color4 = new Color(0x00FFFFFF, true);
assertEquals(255, color4.red);
assertEquals(255, color4.green);
assertEquals(255, color4.blue);
assertEquals(0, color4.alpha);
assertEquals(0x00FFFFFF, color4.rgba());
assertEquals(1, color4.brightness(), 0.0000001);
assertFalse(Color.WHITE.equals(color4));
Color color5 = new Color(0x00010203, false);
assertEquals(1, color5.red);
assertEquals(2, color5.green);
assertEquals(3, color5.blue);
assertEquals(255, color5.alpha);
assertEquals(0xFF010203, color5.rgba());
assertEquals(0.0117647, color5.brightness(), 0.0000001);
Color color6 = new Color(0x00010203, true);
assertEquals(1, color6.red);
assertEquals(2, color6.green);
assertEquals(3, color6.blue);
assertEquals(0, color6.alpha);
assertEquals(0x00010203, color6.rgba());
assertEquals(0.0117647, color6.brightness(), 0.0000001);
}
}
@@ -7,17 +7,15 @@ package com.threerings.getdown.launcher;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLayeredPane;
@@ -890,7 +888,9 @@ public abstract class Getdown extends Thread
_status.setSize(size);
_layers.setPreferredSize(size);
_patchNotes.setBounds(_ifc.patchNotes);
Rectangle patchNotes = new Rectangle(_ifc.patchNotes.x, _ifc.patchNotes.y,
_ifc.patchNotes.width, _ifc.patchNotes.height);
_patchNotes.setBounds(patchNotes);
_patchNotes.setVisible(false);
// we were displaying progress while the UI wasn't up. Now that it is, whatever progress
@@ -1131,7 +1131,7 @@ public abstract class Getdown extends Thread
};
protected Application _app;
protected Application.UpdateInterface _ifc = new Application.UpdateInterface();
protected Application.UpdateInterface _ifc = new Application.UpdateInterface(Config.EMPTY);
protected ResourceBundle _msgs;
protected Container _container;
@@ -142,7 +142,7 @@ public class GetdownApp
});
_frame.setUndecorated(_ifc.hideDecorations);
try {
_frame.setBackground(_ifc.background);
_frame.setBackground(new Color(_ifc.background.rgba(), true));
} catch (UnsupportedOperationException e) {
log.warning("Failed to set background", e);
} catch (IllegalComponentStateException e) {
@@ -6,6 +6,7 @@
package com.threerings.getdown.launcher;
import java.awt.Image;
import java.util.List;
import static com.threerings.getdown.Log.log;
@@ -42,15 +43,15 @@ public class RotatingBackgrounds
* time when the next should be shown. In that case, it's left up until its been there for its
* minimum display time and then the next one gets to come up.
*/
public RotatingBackgrounds (String[] backgrounds, String errorBackground, ImageLoader loader)
public RotatingBackgrounds (List<String> backgrounds, String errorBackground, ImageLoader loader)
{
percentages = new int[backgrounds.length];
minDisplayTime = new int[backgrounds.length];
images = new Image[backgrounds.length];
for (int ii = 0; ii < backgrounds.length; ii++) {
String[] pieces = backgrounds[ii].split(";");
percentages = new int[backgrounds.size()];
minDisplayTime = new int[backgrounds.size()];
images = new Image[backgrounds.size()];
for (int ii = 0; ii < backgrounds.size(); ii++) {
String[] pieces = backgrounds.get(ii).split(";");
if (pieces.length != 2) {
log.warning("Unable to parse background image '" + backgrounds[ii] + "'");
log.warning("Unable to parse background image '" + backgrounds.get(ii) + "'");
makeEmpty();
return;
}
@@ -59,11 +60,11 @@ public class RotatingBackgrounds
minDisplayTime[ii] = Integer.parseInt(pieces[1]);
} catch (NumberFormatException e) {
log.warning("Unable to parse background image display time '" +
backgrounds[ii] + "'");
backgrounds.get(ii) + "'");
makeEmpty();
return;
}
percentages[ii] = (int)((ii/(float)backgrounds.length) * 100);
percentages[ii] = (int)((ii/(float)backgrounds.size()) * 100);
}
if (errorBackground == null) {
errorImage = images[0];
@@ -64,7 +64,9 @@ public class StatusPanel extends JComponent
int width = img == null ? -1 : img.getWidth(this);
int height = img == null ? -1 : img.getHeight(this);
if (width == -1 || height == -1) {
Rectangle bounds = ifc.progress.union(ifc.status);
Rectangle progress = new Rectangle(ifc.progress.x, ifc.progress.y, ifc.progress.width, ifc.progress.height);
Rectangle status = new Rectangle(ifc.status.x, ifc.status.y, ifc.status.width, ifc.status.height);
Rectangle bounds = progress.union(status);
// assume the x inset defines the frame padding; add it on the left, right, and bottom
_psize = new Dimension(bounds.x + bounds.width + bounds.x,
bounds.y + bounds.height + bounds.x);
@@ -107,7 +109,7 @@ public class StatusPanel extends JComponent
_progress = percent;
if (!_ifc.hideProgressText) {
String msg = MessageFormat.format(get("m.complete"), percent);
_newplab = createLabel(msg, _ifc.progressText);
_newplab = createLabel(msg, new Color(_ifc.progressText.rgba(), true));
}
needsRepaint = true;
}
@@ -132,7 +134,7 @@ public class StatusPanel extends JComponent
int minutes = (int)(remaining / 60), seconds = (int)(remaining % 60);
String remstr = minutes + ":" + ((seconds < 10) ? "0" : "") + seconds;
String msg = MessageFormat.format(get("m.remain"), remstr);
_newrlab = createLabel(msg, _ifc.statusText);
_newrlab = createLabel(msg, new Color(_ifc.statusText.rgba(), true));
}
needsRepaint = true;
@@ -226,7 +228,7 @@ public class StatusPanel extends JComponent
gfx.drawImage(_barimg, _ifc.progress.x, _ifc.progress.y, null);
gfx.setClip(null);
} else {
gfx.setColor(_ifc.progressBar);
gfx.setColor(new Color(_ifc.progressBar.rgba(), true));
gfx.fillRect(_ifc.progress.x, _ifc.progress.y,
_progress * _ifc.progress.width / 100,
_ifc.progress.height);
@@ -270,7 +272,7 @@ public class StatusPanel extends JComponent
status += " .";
}
}
_newlab = createLabel(status, _ifc.statusText);
_newlab = createLabel(status, new Color(_ifc.statusText.rgba(), true));
// set the width of the label to the width specified
int width = _ifc.status.width;
if (width == 0) {
@@ -305,7 +307,7 @@ public class StatusPanel extends JComponent
{
Label label = new Label(text, color, FONT);
if (_ifc.textShadow != null) {
label.setAlternateColor(_ifc.textShadow);
label.setAlternateColor(new Color(_ifc.textShadow.rgba(), true));
label.setStyle(LabelStyleConstants.SHADOW);
}
return label;