Skip to content

Commit 2bd067d

Browse files
authored
Merge pull request #36 from lucee/image-3.0.1.3-fixes
LDEV-6243, LDEV-6244, LDEV-6245: fix race condition, perf improvements
2 parents e92f7da + cd1823f commit 2bd067d

9 files changed

Lines changed: 229 additions & 75 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 3.0.1.3
4+
5+
- [LDEV-6243](https://luceeserver.atlassian.net/browse/LDEV-6243) fix member function race condition under concurrent requests
6+
- [LDEV-6244](https://luceeserver.atlassian.net/browse/LDEV-6244) replace GetApplicationSettings BIF with getCustom() to avoid DummyWSHandler exception spam
7+
- [LDEV-6245](https://luceeserver.atlassian.net/browse/LDEV-6245) performance: remove redundant coder cache, fix double write, reduce Tika MIME detection calls, eliminate exception-driven format matching
8+
39
## 3.0.1.2 (2026-03-26)
410

511
- [LDEV-5129](https://luceeserver.atlassian.net/browse/LDEV-5129) remove unused bundled jars: commons-io (CVE-2024-47554), xmpcore, apiguardian, hamcrest, opentest4j

source/java/src/org/lucee/extension/image/ImageUtil.java

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,19 +54,14 @@
5454

5555
public class ImageUtil {
5656

57-
private static Coder _coder;
58-
5957
private static final boolean useSunCodec = getSunCodec();
6058
private static Class JPEGCodec;
6159
private static Class JPEGEncodeParam;
6260

6361
private static int counter = 0;
6462

6563
private static Coder getCoder() {
66-
if (true || _coder == null) {
67-
_coder = Coder.getInstance(CFMLEngineFactory.getInstance().getThreadPageContext());
68-
}
69-
return _coder;
64+
return Coder.getInstance(CFMLEngineFactory.getInstance().getThreadPageContext());
7065
}
7166

7267
public static String getOneWriterFormatName(String... preferences) throws IOException {
@@ -179,23 +174,34 @@ public static byte[] readBase64(String b64str, StringBuilder mimetype) throws IO
179174
return Base64.decodeBase64(b64str.getBytes());
180175
}
181176

177+
/**
178+
* Detect the image format for a resource. The detection order is:
179+
* 1. Coder-specific magic byte detection (JDeli, TwelveMonkeys etc) — handles misnamed files
180+
* 2. MIME type detection via Tika (content-based) — fallback for formats coders don't recognise
181+
* 3. File extension — last resort, trusts the filename
182+
*
183+
* MIME type (Tika) is resolved once and passed to all coders to avoid repeated detection.
184+
* This is critical for performance — Tika's magic byte scanning is expensive (~600k allocations
185+
* per 5k image ops when called per-coder).
186+
*
187+
* Misnamed files (e.g. a JPEG saved as .png) are handled by steps 1 and 2 which inspect
188+
* actual file content, not the extension.
189+
*/
182190
public static String getFormat(Resource res) throws IOException {
183191
long len = res.length();
192+
// resolve MIME type once and pass to coders — avoids repeated Tika detection
193+
String mt = len > 0 ? getMimeType(res, null) : null;
184194
Coder c = getCoder();
185195
if (c instanceof FormatExtract) {
186-
String format = ((FormatExtract) c).getFormat(res, null);
196+
String format = ((FormatExtract) c).getFormat(res, mt, null);
187197
if (!Util.isEmpty(format, true)) {
188198
return format;
189199
}
190200
}
191-
// there is no need to check the mime type if the file is empty
192-
if (len > 0) {
193-
String mt = getMimeType(res, null);
194-
if (!Util.isEmpty(mt)) {
195-
String format = getImageFormatFromMimeType(mt, null);
196-
if (!Util.isEmpty(format)) {
197-
return format;
198-
}
201+
if (!Util.isEmpty(mt)) {
202+
String format = getImageFormatFromMimeType(mt, null);
203+
if (!Util.isEmpty(format)) {
204+
return format;
199205
}
200206
}
201207
return getFormatFromExtension(res, null);
@@ -210,21 +216,23 @@ public static String getMimeType(byte[] binary, String defaultValue) {
210216
}
211217

212218
public static String getFormat(byte[] binary) throws IOException {
219+
String mt = CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, "");
213220
Coder c = getCoder();
214221
if (c instanceof FormatExtract) {
215-
String format = ((FormatExtract) c).getFormat(binary, null);
222+
String format = ((FormatExtract) c).getFormat(binary, mt, null);
216223
if (!Util.isEmpty(format, true)) return format;
217224
}
218-
return getFormatFromMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, ""));
225+
return getFormatFromMimeType(mt);
219226
}
220227

221228
public static String getFormat(byte[] binary, String defaultValue) {
229+
String mt = CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, "");
222230
Coder c = getCoder();
223231
if (c instanceof FormatExtract) {
224-
String format = ((FormatExtract) c).getFormat(binary, null);
232+
String format = ((FormatExtract) c).getFormat(binary, mt, null);
225233
if (!Util.isEmpty(format, true)) return format;
226234
}
227-
return getImageFormatFromMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(binary, ""), defaultValue);
235+
return getImageFormatFromMimeType(mt, defaultValue);
228236
}
229237

230238
public static String toFormat(String format) {

source/java/src/org/lucee/extension/image/coder/AImageIOInterface.java

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ public BufferedImage read(byte[] bytes, String format) throws IOException {
110110

111111
@Override
112112
public void write(Image img, Resource destination, String format, float quality, boolean noMeta) throws IOException {
113-
if (destination instanceof File) writeImage(img, destination, format, quality, noMeta);
113+
if (destination instanceof File) {
114+
writeImage(img, destination, format, quality, noMeta);
115+
return;
116+
}
114117
OutputStream os = null;
115118
try {
116119
os = destination.getOutputStream();
@@ -169,6 +172,14 @@ public String getFormat(Resource res, String defaultValue) {
169172

170173
}
171174

175+
@Override
176+
public String getFormat(Resource res, String mimeType, String defaultValue) {
177+
if (!Util.isEmpty(mimeType)) {
178+
return getFormatbyMimeType(mimeType, defaultValue);
179+
}
180+
return getFormat(res, defaultValue);
181+
}
182+
172183
@Override
173184
public String getFormat(byte[] bytes) throws IOException {
174185
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null));
@@ -179,30 +190,30 @@ public String getFormat(byte[] bytes, String defaultValue) {
179190
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null), defaultValue);
180191
}
181192

182-
private String getFormatbyMimeType(String mimeType, String defaultValue) {
193+
@Override
194+
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
183195
if (!Util.isEmpty(mimeType)) {
184-
try {
185-
return getFormatbyMimeType(mimeType);
186-
}
187-
catch (Throwable t) {
188-
if (t instanceof ThreadDeath) throw (ThreadDeath) t;
189-
}
196+
return getFormatbyMimeType(mimeType, defaultValue);
190197
}
191-
return defaultValue;
198+
return getFormat(bytes, defaultValue);
192199
}
193200

194-
private String getFormatbyMimeType(String mimeType) throws IOException {
201+
private String getFormatbyMimeType(String mimeType, String defaultValue) {
195202
if (!Util.isEmpty(mimeType)) {
196-
197203
for (Map.Entry<String, Codec> e: codecs.entrySet()) {
198204
for (String mt: e.getValue().mimeTypes) {
199-
200205
if (mimeType.equalsIgnoreCase(mt)) {
201206
return e.getKey();
202207
}
203208
}
204209
}
205210
}
211+
return defaultValue;
212+
}
213+
214+
private String getFormatbyMimeType(String mimeType) throws IOException {
215+
String result = getFormatbyMimeType(mimeType, null);
216+
if (result != null) return result;
206217
throw new IOException("no matching format found for mimetype [" + mimeType + "]");
207218
}
208219

source/java/src/org/lucee/extension/image/coder/ImageIOCoder.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ public String getFormat(Resource res, String defaultValue) {
139139
return defaultValue;
140140
}
141141

142+
@Override
143+
public String getFormat(Resource res, String mimeType, String defaultValue) {
144+
if (!Util.isEmpty(mimeType)) {
145+
return getFormatbyMimeType(mimeType, defaultValue);
146+
}
147+
return getFormat(res, defaultValue);
148+
}
149+
142150
@Override
143151
public String getFormat(byte[] bytes) throws IOException {
144152
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null));
@@ -149,27 +157,31 @@ public String getFormat(byte[] bytes, String defaultValue) {
149157
return getFormatbyMimeType(CFMLEngineFactory.getInstance().getResourceUtil().getMimeType(bytes, null), defaultValue);
150158
}
151159

160+
@Override
161+
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
162+
if (!Util.isEmpty(mimeType)) {
163+
return getFormatbyMimeType(mimeType, defaultValue);
164+
}
165+
return getFormat(bytes, defaultValue);
166+
}
167+
152168
private String getFormatbyMimeType(String mimeType, String defaultValue) {
153169
if (!Util.isEmpty(mimeType)) {
154170
try {
155-
return getFormatbyMimeType(mimeType);
171+
Iterator<ImageReader> it = ImageIO.getImageReadersByMIMEType(mimeType);
172+
while (it != null && it.hasNext()) {
173+
return it.next().getFormatName();
174+
}
156175
}
157-
catch (Throwable t) {
158-
if (t instanceof ThreadDeath) throw (ThreadDeath) t;
176+
catch (IOException e) {
159177
}
160178
}
161179
return defaultValue;
162180
}
163181

164182
private String getFormatbyMimeType(String mimeType) throws IOException {
165-
if (!Util.isEmpty(mimeType)) {
166-
Iterator<ImageReader> it = ImageIO.getImageReadersByMIMEType(mimeType);
167-
while (it != null && it.hasNext()) {
168-
String fn = it.next().getFormatName();
169-
return fn;
170-
}
171-
172-
}
183+
String result = getFormatbyMimeType(mimeType, null);
184+
if (result != null) return result;
173185
throw new IOException("no matching format found for mimetype [" + mimeType + "]");
174186
}
175187

source/java/src/org/lucee/extension/image/coder/MultiCoder.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,14 @@ public String getFormat(byte[] bytes) throws IOException {
276276

277277
@Override
278278
public String getFormat(Resource res, String defaultValue) {
279+
return getFormat(res, null, defaultValue);
280+
}
281+
282+
@Override
283+
public String getFormat(Resource res, String mimeType, String defaultValue) {
279284
for (Coder coder: coders) {
280285
if (!(coder instanceof FormatExtract)) continue;
281-
String format = ((FormatExtract) coder).getFormat(res, null);
286+
String format = ((FormatExtract) coder).getFormat(res, mimeType, null);
282287
if (!Util.isEmpty(format)) {
283288
return format;
284289
}
@@ -288,9 +293,14 @@ public String getFormat(Resource res, String defaultValue) {
288293

289294
@Override
290295
public String getFormat(byte[] bytes, String defaultValue) {
296+
return getFormat(bytes, null, defaultValue);
297+
}
298+
299+
@Override
300+
public String getFormat(byte[] bytes, String mimeType, String defaultValue) {
291301
for (Coder coder: coders) {
292302
if (!(coder instanceof FormatExtract)) continue;
293-
String format = ((FormatExtract) coder).getFormat(bytes, null);
303+
String format = ((FormatExtract) coder).getFormat(bytes, mimeType, null);
294304
if (!Util.isEmpty(format)) return format;
295305
}
296306
return defaultValue;

source/java/src/org/lucee/extension/image/format/FormatExtract.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,13 @@ public interface FormatExtract {
1212
public abstract String getFormat(byte[] bytes) throws IOException;
1313

1414
public abstract String getFormat(byte[] bytes, String defaultValue);
15+
16+
// overloads that accept a pre-resolved MIME type to avoid repeated Tika detection
17+
default String getFormat(Resource res, String mimeType, String defaultValue) {
18+
return getFormat(res, defaultValue);
19+
}
20+
21+
default String getFormat(byte[] bytes, String mimeType, String defaultValue) {
22+
return getFormat(bytes, defaultValue);
23+
}
1524
}

source/java/src/org/lucee/extension/image/util/CommonUtil.java

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ public class CommonUtil {
3636
public static final short UNDEFINED_NODE = -1;
3737
private static final String _8220 = String.valueOf((char) 8220);
3838

39-
private static Map<Collection.Key, Coll> members;
40-
private static BIF GetApplicationSettings;
39+
private static volatile Map<Collection.Key, Coll> members;
40+
private static final Object membersLock = new Object();
4141

4242
public static String unwrap(String str) {
4343
if (str == null) return "";
@@ -156,31 +156,34 @@ else if (res.isDirectory()) {
156156

157157
public static Map<Collection.Key, Coll> getMembers(PageContext pc) throws PageException {
158158
if (members == null) {
159-
Cast cast = CFMLEngineFactory.getInstance().getCastUtil();
160-
members = new HashMap<Collection.Key, Coll>();
161-
ConfigWeb config = pc.getConfig();
162-
Object[] flds = getFLDs(config, 1);
163-
Map funcs;
164-
Iterator it;
165-
Object func;
166-
String[] names;
167-
boolean chaining;
168-
BIF bif;
169-
Coll coll;
170-
for (int i = 0; i < flds.length; i++) {
171-
funcs = getFunctions(flds[i]);
172-
it = funcs.values().iterator();
173-
while (it.hasNext()) {
174-
func = it.next();
175-
if (getMemberType(func) == Image.TYPE_IMAGE) {
176-
names = getMemberNames(func);
177-
if (names != null && names.length > 0) {
178-
coll = new Coll(getBIF(func), getMemberChaining(func));
179-
for (String name: names) {
180-
members.put(cast.toKey(name), coll);
159+
synchronized (membersLock) {
160+
if (members == null) {
161+
Cast cast = CFMLEngineFactory.getInstance().getCastUtil();
162+
Map<Collection.Key, Coll> local = new HashMap<>();
163+
ConfigWeb config = pc.getConfig();
164+
Object[] flds = getFLDs(config, 1);
165+
Map funcs;
166+
Iterator it;
167+
Object func;
168+
String[] names;
169+
Coll coll;
170+
for (int i = 0; i < flds.length; i++) {
171+
funcs = getFunctions(flds[i]);
172+
it = funcs.values().iterator();
173+
while (it.hasNext()) {
174+
func = it.next();
175+
if (getMemberType(func) == Image.TYPE_IMAGE) {
176+
names = getMemberNames(func);
177+
if (names != null && names.length > 0) {
178+
coll = new Coll(getBIF(func), getMemberChaining(func));
179+
for (String name: names) {
180+
local.put(cast.toKey(name), coll);
181+
}
182+
}
181183
}
182184
}
183185
}
186+
members = local;
184187
}
185188
}
186189
}
@@ -268,22 +271,34 @@ public static void close(ImageInputStream iis) {
268271
}
269272
}
270273

274+
private static Method getCustomMethod;
275+
271276
public static Set<String> getCoders(StringBuilder sb, PageContext pc) {
272277
Set<String> result = null;
273278
try {
274279
CFMLEngine eng = CFMLEngineFactory.getInstance();
275280
if (pc == null) pc = eng.getThreadPageContext();
276281
if (pc == null) return null;
277-
if (GetApplicationSettings == null) {
278-
GetApplicationSettings = eng.getClassUtil().loadBIF(pc, "lucee.runtime.functions.system.GetApplicationSettings");
282+
283+
Object ac = pc.getApplicationContext();
284+
if (ac == null) return null;
285+
286+
// read this.image from ApplicationContext via getCustom(Key)
287+
Object o = null;
288+
if (getCustomMethod == null || getCustomMethod.getDeclaringClass() != ac.getClass()) {
289+
try {
290+
getCustomMethod = ac.getClass().getMethod("getCustom", new Class[] { Collection.Key.class });
291+
}
292+
catch (NoSuchMethodException e) {
293+
return null;
294+
}
279295
}
280-
Struct sct = (Struct) GetApplicationSettings.invoke(pc, new Object[] { Boolean.TRUE });
281-
Object o = sct.get("image", null);
296+
o = getCustomMethod.invoke(ac, new Object[] { eng.getCastUtil().toKey("image") });
297+
282298
if (o instanceof Struct) {
283299
Struct image = (Struct) o;
284-
// type
285300
o = image.get("coder", null);
286-
if (o == null) image.get("coders", null);
301+
if (o == null) o = image.get("coders", null);
287302

288303
if (o != null && eng.getDecisionUtil().isCastableToArray(o)) {
289304
String[] coders = eng.getListUtil().toStringArray(eng.getCastUtil().toArray(o));
@@ -295,7 +310,6 @@ public static Set<String> getCoders(StringBuilder sb, PageContext pc) {
295310
}
296311
}
297312
}
298-
299313
}
300314
catch (Exception e) {
301315
Coder.log(pc);

0 commit comments

Comments
 (0)