java-topology/test/jdk/java/text/Format/DecimalFormat/CloneTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

156 lines
6.1 KiB
Java

/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8354522 8358880 8367324
* @summary Check for cloning interference
* @library /test/lib
* @run junit/othervm --add-opens=java.base/java.text=ALL-UNNAMED CloneTest
*/
import jtreg.SkippedException;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
public class CloneTest {
// Note: this is a white-box test that may fail if the implementation is changed
@Test
public void testClone() {
DecimalFormat df = new DecimalFormat("#");
new CloneTester(df).testClone();
}
// Note: this is a white-box test that may fail if the implementation is changed
@Test
public void testCloneAfterInit() {
DecimalFormat df = new DecimalFormat("#");
// This initial use of the formatter initialises its internal state, which could
// subsequently be shared across clones. This is key to reproducing this specific
// issue.
String _ = df.format(Math.PI);
new CloneTester(df).testClone();
}
private static class CloneTester {
private final Field digitListField;
private final Class<?> digitListClass;
private final DecimalFormat original;
public CloneTester(DecimalFormat original) {
this.original = original;
try {
digitListField = DecimalFormat.class.getDeclaredField("digitList");
digitListField.setAccessible(true);
DecimalFormat df = new DecimalFormat();
Object digitList = digitListField.get(df);
digitListClass = digitList.getClass();
} catch (ReflectiveOperationException e) {
throw new SkippedException("reflective access in white-box test failed", e);
}
}
public void testClone() {
try {
DecimalFormat dfClone = (DecimalFormat) original.clone();
Object digits = valFromDigitList(original, "digits");
assertNotSame(digits, valFromDigitList(dfClone, "digits"));
assertEquals(digitListField.get(original), digitListField.get(dfClone));
} catch (ReflectiveOperationException e) {
throw new SkippedException("reflective access in white-box test failed", e);
}
}
private Object valFromDigitList(DecimalFormat df, String fieldName) throws ReflectiveOperationException {
Object digitList = digitListField.get(df);
Field field = digitListClass.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(digitList);
}
}
// Tests that when DecimalFormat is cloned after use with
// a long double/BigDecimal, clones will be independent. This is not an
// exhaustive test. This tests for the issue of the same DigitList.data
// array being reused across clones of DecimalFormat.
@Test
public void testCloneIndependence() {
AtomicInteger mismatchCount = new AtomicInteger(0);
DecimalFormat df = new DecimalFormat("#");
CountDownLatch startSignal = new CountDownLatch(1);
// This initial use of the formatter initialises its internal state, which could
// subsequently be shared across clones. This is key to reproducing this specific
// issue.
String _ = df.format(Math.PI);
try (var ex = Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())) {
for (int i = 0; i < 5; i++) {
final int finalI = i;
// Each thread gets its own clone of df
DecimalFormat threadDf = (DecimalFormat) df.clone();
Runnable task = () -> {
try {
startSignal.await();
for (int j = 0; j < 1_000; j++) {
if (mismatchCount.get() > 0) {
// Exit early if mismatch has already occurred
break;
}
int value = finalI * j;
String dfString = threadDf.format(BigDecimal.valueOf(value));
String str = String.valueOf(value);
if (!str.equals(dfString)) {
mismatchCount.getAndIncrement();
System.err.println("mismatch: str = " + str + " dfString = " + dfString);
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
};
ex.execute(task);
}
startSignal.countDown(); // let all tasks start working at the same time
}
assertEquals(0, mismatchCount.get());
}
}