Skip to content

Commit f37f09f

Browse files
shai-almogclaude
andauthored
simulator: guard window-bounds prefs against corruption from Larger Text scaling (#4935)
Picking Simulate -> Larger Text -> Extra Extra Extra Large (and the larger Accessibility steps) could collapse the simulator frame; refreshSkin()'s pack() produced an unusably small geometry that the existing componentResized listener wrote straight into prefs. The next launch restored the bad bounds, leaving the window stuck. Add a shared validator (JavaSEPort.isUsableWindowBounds) used on both write and read sides to reject degenerate, oversize, or off-screen rectangles, and extract parsePersistedBounds so a malformed pref no longer aborts init() with a NumberFormatException. On read, corrupt entries are removed so a single bad event does not survive across launches. AppPanel applies the same guards to its per-panel preferredWindowBounds keys. Adds JavaSEPortWindowBoundsTest covering null/tiny/huge/off-screen inputs, parse rejection of malformed strings, and a write/parse roundtrip. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2883366 commit f37f09f

3 files changed

Lines changed: 267 additions & 17 deletions

File tree

Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ public class JavaSEPort extends CodenameOneImplementation {
206206
private boolean largerTextEnabled = false;
207207
private static final String PREF_LARGER_TEXT_SCALE = "cn1.simulator.largerTextScale";
208208

209+
// Floor below which any persisted window dimension is treated as the
210+
// product of a layout glitch (eg. a pack() with a collapsed canvas, an
211+
// iconified frame, or hand-edited prefs) rather than a real user state.
212+
static final int MIN_PERSISTED_WINDOW_DIMENSION = 100;
213+
214+
// Defensive ceiling so absurd values from a corrupted pref or runaway
215+
// scaling math cannot make a subsequent setBounds() throw or place the
216+
// frame off every reachable display.
217+
static final int MAX_PERSISTED_WINDOW_DIMENSION = 32767;
218+
209219
static {
210220
IOS_NATIVE_FONT_CANDIDATES.put("native:MainThin", new String[] {
211221
"SF Pro Display", "SF Pro Text",
@@ -5825,6 +5835,75 @@ public void run() {
58255835
});
58265836
}
58275837

5838+
/**
5839+
* Returns true when {@code r} represents a window position we are willing
5840+
* to persist or restore. Rejects null, NaN-equivalent (non-positive) sizes,
5841+
* sub-floor dimensions, oversized values, and frames that no longer have a
5842+
* useful overlap with any connected display. Any caller that fails this
5843+
* check should fall back to default bounds and remove the corrupt pref so
5844+
* the bad state does not survive across launches.
5845+
*/
5846+
public static boolean isUsableWindowBounds(Rectangle r) {
5847+
if (r == null) {
5848+
return false;
5849+
}
5850+
if (r.width < MIN_PERSISTED_WINDOW_DIMENSION || r.height < MIN_PERSISTED_WINDOW_DIMENSION) {
5851+
return false;
5852+
}
5853+
if (r.width > MAX_PERSISTED_WINDOW_DIMENSION || r.height > MAX_PERSISTED_WINDOW_DIMENSION) {
5854+
return false;
5855+
}
5856+
try {
5857+
if (GraphicsEnvironment.isHeadless()) {
5858+
// No screens to validate against; size has already cleared the
5859+
// sanity floor, so accept. The simulator is not really expected
5860+
// to run headless, but tests do.
5861+
return true;
5862+
}
5863+
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
5864+
GraphicsDevice[] devs = ge.getScreenDevices();
5865+
if (devs == null || devs.length == 0) {
5866+
return true;
5867+
}
5868+
Rectangle union = new Rectangle(0, 0, 0, 0);
5869+
for (GraphicsDevice gd : devs) {
5870+
union.add(gd.getDefaultConfiguration().getBounds());
5871+
}
5872+
Rectangle visible = union.intersection(r);
5873+
// Require enough overlap that the title bar and drag region remain
5874+
// reachable; a one-pixel touch is not good enough to "remember".
5875+
return visible.width >= MIN_PERSISTED_WINDOW_DIMENSION && visible.height >= 50;
5876+
} catch (Throwable t) {
5877+
return false;
5878+
}
5879+
}
5880+
5881+
/**
5882+
* Parses the comma-separated {@code "x,y,width,height"} form written to
5883+
* {@code window.bounds}. Returns {@code null} when the input is missing,
5884+
* has the wrong field count, or contains non-numeric data. The bounds
5885+
* returned have not been validated for screen overlap or dimensions;
5886+
* pair with {@link #isUsableWindowBounds(Rectangle)}.
5887+
*/
5888+
static Rectangle parsePersistedBounds(String s) {
5889+
if (s == null) {
5890+
return null;
5891+
}
5892+
try {
5893+
String[] parts = s.split(",");
5894+
if (parts.length != 4) {
5895+
return null;
5896+
}
5897+
return new Rectangle(
5898+
Integer.parseInt(parts[0].trim()),
5899+
Integer.parseInt(parts[1].trim()),
5900+
Integer.parseInt(parts[2].trim()),
5901+
Integer.parseInt(parts[3].trim()));
5902+
} catch (NumberFormatException e) {
5903+
return null;
5904+
}
5905+
}
5906+
58285907

58295908
private ArrayList<Runnable> deinitializeHooks = new ArrayList<>();
58305909
public void addDeinitializeHook(Runnable r) {
@@ -6249,9 +6328,17 @@ public void windowDeactivated(WindowEvent e) {
62496328
private void saveBounds(ComponentEvent e) {
62506329
if (e.getComponent() instanceof JFrame) {
62516330
Frame f = (JFrame)e.getComponent();
6331+
// The NORMAL check filters maximized/iconified states,
6332+
// but a misbehaving pack() can still produce tiny or
6333+
// off-screen geometry while remaining NORMAL. Run the
6334+
// shared sanity check so a broken layout pass cannot
6335+
// corrupt the persisted bounds.
62526336
if (f.getExtendedState() == JFrame.NORMAL) {
6253-
Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class);
62546337
Rectangle bounds = f.getBounds();
6338+
if (!isUsableWindowBounds(bounds)) {
6339+
return;
6340+
}
6341+
Preferences pref = Preferences.userNodeForPackage(JavaSEPort.class);
62556342
pref.put("window.bounds", bounds.x+","+bounds.y+","+bounds.width+","+bounds.height);
62566343
}
62576344
}
@@ -6360,18 +6447,13 @@ public void componentHidden(ComponentEvent e) {
63606447
}
63616448
String lastBounds = pref.get("window.bounds", null);
63626449
if (lastBounds != null) {
6363-
String[] parts = lastBounds.split(",");
6364-
Rectangle r = new Rectangle(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2]), Integer.parseInt(parts[3]));
6365-
Rectangle bounds = new Rectangle(0, 0, 0, 0);
6366-
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
6367-
GraphicsDevice lstGDs[] = ge.getScreenDevices();
6368-
for (GraphicsDevice gd : lstGDs) {
6369-
bounds.add(gd.getDefaultConfiguration().getBounds());
6370-
}
6371-
6372-
if (bounds.intersects(r)) {
6373-
6450+
Rectangle r = parsePersistedBounds(lastBounds);
6451+
if (isUsableWindowBounds(r)) {
63746452
window.setBounds(r);
6453+
} else {
6454+
// Drop the bad value so a single bad pack() cannot lock the
6455+
// user out across restarts.
6456+
pref.remove("window.bounds");
63756457
}
63766458
}
63776459

Ports/JavaSE/src/com/codename1/impl/javase/simulator/AppPanel.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,11 @@ public void savePreferences(AppFrame frame, Preferences prefs) {
311311
JFrame window = frame.getWindow(this);
312312
if (window != null) {
313313
Rectangle r = window.getBounds();
314+
// Skip persistence when the geometry is degenerate: a layout
315+
// glitch or minimized frame must not corrupt the saved state.
316+
if (!JavaSEPort.isUsableWindowBounds(r)) {
317+
return;
318+
}
314319
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.x", r.x);
315320
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.y", r.y);
316321
prefs.putInt(getPreferencesPrefix(frame) + "preferredWindowBounds.width", r.width);
@@ -327,11 +332,23 @@ public void applyPreferences(AppFrame frame, Preferences prefs) {
327332
setPreferredFrame(AppFrame.FrameLocation.valueOf(preferredFrameName));
328333
} catch (IllegalArgumentException ex){}
329334
}
330-
preferredWindowBounds = new Rectangle(0, 0, getPreferredSize().width, getPreferredSize().height);
331-
preferredWindowBounds.x = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.x", preferredWindowBounds.x);
332-
preferredWindowBounds.y = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.y", preferredWindowBounds.y);
333-
preferredWindowBounds.width = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.width", preferredWindowBounds.width);
334-
preferredWindowBounds.height = prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.height", preferredWindowBounds.height);
335+
Rectangle defaults = new Rectangle(0, 0, getPreferredSize().width, getPreferredSize().height);
336+
Rectangle loaded = new Rectangle(
337+
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.x", defaults.x),
338+
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.y", defaults.y),
339+
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.width", defaults.width),
340+
prefs.getInt(getPreferencesPrefix(frame)+"preferredWindowBounds.height", defaults.height));
341+
if (JavaSEPort.isUsableWindowBounds(loaded)) {
342+
preferredWindowBounds = loaded;
343+
} else {
344+
preferredWindowBounds = defaults;
345+
// Clear the corrupt entries so a one-time glitch does not haunt
346+
// every future launch of the simulator.
347+
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.x");
348+
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.y");
349+
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.width");
350+
prefs.remove(getPreferencesPrefix(frame)+"preferredWindowBounds.height");
351+
}
335352
preferredAlwaysOnTop = prefs.getBoolean(getPreferencesPrefix(frame)+"preferredAlwaysOnTop", preferredAlwaysOnTop);
336353

337354

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package com.codename1.impl.javase;
2+
3+
import java.awt.GraphicsDevice;
4+
import java.awt.GraphicsEnvironment;
5+
import java.awt.Rectangle;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertNotNull;
12+
import static org.junit.jupiter.api.Assertions.assertNull;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
15+
16+
/**
17+
* Guards against regressions in the simulator's window-bounds persistence.
18+
* The bug that prompted these tests: picking Simulate -> Larger Text ->
19+
* Extra Extra Extra Large collapsed the frame, the resulting tiny geometry
20+
* was written to prefs, and every subsequent launch restored the unusable
21+
* window. The helpers tested here are the choke points that must reject
22+
* those values on both write and read.
23+
*/
24+
public class JavaSEPortWindowBoundsTest {
25+
26+
@Test
27+
public void isUsableWindowBoundsRejectsNull() {
28+
assertFalse(JavaSEPort.isUsableWindowBounds(null));
29+
}
30+
31+
@Test
32+
public void isUsableWindowBoundsRejectsCollapsedFrame() {
33+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 0, 0)));
34+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 1, 1)));
35+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 50, 50)));
36+
}
37+
38+
@Test
39+
public void isUsableWindowBoundsRejectsJustBelowFloor() {
40+
// 99 sits just under MIN_PERSISTED_WINDOW_DIMENSION (100). Verifies the
41+
// floor is enforced strictly, not as "approximately".
42+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 99, 200)));
43+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 200, 99)));
44+
}
45+
46+
@Test
47+
public void isUsableWindowBoundsRejectsAbsurdSize() {
48+
// A corrupt or overflowing pref could produce arbitrarily large values.
49+
// Both dimensions are checked independently.
50+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 40000, 800)));
51+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, 800, 40000)));
52+
assertFalse(JavaSEPort.isUsableWindowBounds(new Rectangle(0, 0, Integer.MAX_VALUE, Integer.MAX_VALUE)));
53+
}
54+
55+
@Test
56+
public void isUsableWindowBoundsAcceptsReasonableOnDefaultScreen() {
57+
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
58+
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
59+
Rectangle screen = gd.getDefaultConfiguration().getBounds();
60+
Rectangle window = new Rectangle(screen.x + 50, screen.y + 50, 800, 600);
61+
assertTrue(JavaSEPort.isUsableWindowBounds(window),
62+
"a normally placed 800x600 window on the default screen must be accepted");
63+
}
64+
65+
@Test
66+
public void isUsableWindowBoundsRejectsFarOffScreen() {
67+
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
68+
// No reachable display extends out this far; the user could never
69+
// recover this window with the mouse.
70+
Rectangle window = new Rectangle(-100000, -100000, 800, 600);
71+
assertFalse(JavaSEPort.isUsableWindowBounds(window));
72+
}
73+
74+
@Test
75+
public void isUsableWindowBoundsRejectsSliverOnScreen() {
76+
assumeFalse(GraphicsEnvironment.isHeadless(), "needs a display");
77+
GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
78+
Rectangle screen = gd.getDefaultConfiguration().getBounds();
79+
// Window is mostly off the right edge of the screen, leaving only ~10px
80+
// visible. The old intersects() check accepted this; the new floor of
81+
// 100px visible width must reject it.
82+
Rectangle window = new Rectangle(screen.x + screen.width - 10, screen.y + 50, 800, 600);
83+
assertFalse(JavaSEPort.isUsableWindowBounds(window));
84+
}
85+
86+
@Test
87+
public void parsePersistedBoundsAcceptsWellFormed() {
88+
Rectangle r = JavaSEPort.parsePersistedBounds("10,20,800,600");
89+
assertNotNull(r);
90+
assertEquals(10, r.x);
91+
assertEquals(20, r.y);
92+
assertEquals(800, r.width);
93+
assertEquals(600, r.height);
94+
}
95+
96+
@Test
97+
public void parsePersistedBoundsToleratesWhitespace() {
98+
// Defensive against hand-edited prefs or future formatting tweaks; the
99+
// current writer does not insert spaces but readers should not be
100+
// fragile about it.
101+
Rectangle r = JavaSEPort.parsePersistedBounds(" 10 , 20 , 800 , 600 ");
102+
assertNotNull(r);
103+
assertEquals(10, r.x);
104+
assertEquals(600, r.height);
105+
}
106+
107+
@Test
108+
public void parsePersistedBoundsAcceptsNegativeOrigin() {
109+
// Multi-monitor setups can legitimately place a window at negative
110+
// coordinates relative to the primary display.
111+
Rectangle r = JavaSEPort.parsePersistedBounds("-1200,-300,800,600");
112+
assertNotNull(r);
113+
assertEquals(-1200, r.x);
114+
assertEquals(-300, r.y);
115+
}
116+
117+
@Test
118+
public void parsePersistedBoundsRejectsNull() {
119+
assertNull(JavaSEPort.parsePersistedBounds(null));
120+
}
121+
122+
@Test
123+
public void parsePersistedBoundsRejectsEmpty() {
124+
assertNull(JavaSEPort.parsePersistedBounds(""));
125+
}
126+
127+
@Test
128+
public void parsePersistedBoundsRejectsWrongFieldCount() {
129+
assertNull(JavaSEPort.parsePersistedBounds("10,20,30"));
130+
assertNull(JavaSEPort.parsePersistedBounds("10,20,30,40,50"));
131+
assertNull(JavaSEPort.parsePersistedBounds("10"));
132+
}
133+
134+
@Test
135+
public void parsePersistedBoundsRejectsNonNumeric() {
136+
// A NumberFormatException leaking out of init() would abort the entire
137+
// simulator startup; the parser must swallow it and return null.
138+
assertNull(JavaSEPort.parsePersistedBounds("not,a,number,here"));
139+
assertNull(JavaSEPort.parsePersistedBounds("10,20,800,abc"));
140+
assertNull(JavaSEPort.parsePersistedBounds("10.5,20,800,600"));
141+
}
142+
143+
@Test
144+
public void roundTripWriteAndParse() {
145+
// Mirrors the save format used by saveBounds(): "x,y,width,height".
146+
Rectangle saved = new Rectangle(150, 75, 1024, 768);
147+
String written = saved.x + "," + saved.y + "," + saved.width + "," + saved.height;
148+
Rectangle restored = JavaSEPort.parsePersistedBounds(written);
149+
assertEquals(saved, restored);
150+
}
151+
}

0 commit comments

Comments
 (0)