From 86cd035c10c211ba7cb2e4ceff4fa286ccf8fe78 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Mon, 2 Feb 2026 21:42:10 +0100 Subject: [PATCH 1/3] Proper fixup for " when generating CQL query The replaceAll for looking back whether the " was escaped did not work as the preceding backslash could have been the result of escaping a backslash itself. A follow up for #5 --- .../java/org/z3950/zing/cql/CQLLexer.java | 5 +- .../java/org/z3950/zing/cql/CQLTermNode.java | 35 +++++++------ .../org/z3950/zing/cql/CQLTermNodeTest.java | 52 +++++++++++++++++++ 3 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java diff --git a/src/main/java/org/z3950/zing/cql/CQLLexer.java b/src/main/java/org/z3950/zing/cql/CQLLexer.java index c27ba40..a598b01 100644 --- a/src/main/java/org/z3950/zing/cql/CQLLexer.java +++ b/src/main/java/org/z3950/zing/cql/CQLLexer.java @@ -18,6 +18,8 @@ public class CQLLexer implements CQLTokenizer { private String lval; private StringBuilder buf = new StringBuilder(); + static final String OPS_AND_WHITESPACE = "()/<>= \t\r\n"; + public CQLLexer(String cql, boolean debug) { qs = cql; ql = cql.length(); @@ -89,8 +91,7 @@ public void move() { } else { what = TT_WORD; buf.setLength(0); // reset buffer - while (qi < ql - && !strchr("()/<>= \t\r\n", qs.charAt(qi))) { + while (qi < ql && !strchr(OPS_AND_WHITESPACE, qs.charAt(qi))) { buf.append(qs.charAt(qi)); qi++; } diff --git a/src/main/java/org/z3950/zing/cql/CQLTermNode.java b/src/main/java/org/z3950/zing/cql/CQLTermNode.java index 9e6551d..22c644f 100644 --- a/src/main/java/org/z3950/zing/cql/CQLTermNode.java +++ b/src/main/java/org/z3950/zing/cql/CQLTermNode.java @@ -223,24 +223,27 @@ public String toPQF(Properties config) throws PQFTranslationException { } static String maybeQuote(String str) { - if (str == null) + if (str == null) { return null; - - // There _must_ be a better way to make this test ... - if (str.length() == 0 || - str.indexOf('"') != -1 || - str.indexOf(' ') != -1 || - str.indexOf('\t') != -1 || - str.indexOf('=') != -1 || - str.indexOf('<') != -1 || - str.indexOf('>') != -1 || - str.indexOf('/') != -1 || - str.indexOf('(') != -1 || - str.indexOf(')') != -1) { - str = '"' + str.replaceAll("(?= 0) { + quote = true; + } + if (ch == '"' && !escaped) { + sb.append('\\'); + } + escaped = ch == '\\' && !escaped; + sb.append(ch); + } + if (quote) { + return "\"" + sb.toString() + "\""; + } else { + return sb.toString(); + } } @Override diff --git a/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java new file mode 100644 index 0000000..4bdda59 --- /dev/null +++ b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java @@ -0,0 +1,52 @@ +package org.z3950.zing.cql; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class CQLTermNodeTest { + @Test + public void TestMaybeQuoteNull() { + assertNull(CQLTermNode.maybeQuote(null)); + } + + @Test + public void TestMaybeQuoteEmpty() { + assertEquals("\"\"", CQLTermNode.maybeQuote("")); + } + + @Test + public void TestMaybeQuoteRelation() { + assertEquals("\"<\"", CQLTermNode.maybeQuote("<")); + } + + @Test + public void TestMaybeQuoteSimple() { + assertEquals("simple", CQLTermNode.maybeQuote("simple")); + } + + @Test + public void TestMaybeQuoteBlank() { + assertEquals("\"a b\"", CQLTermNode.maybeQuote("a b")); + } + + @Test + public void TestMaybeQuoteQuote1() { + assertEquals("a\\\"", CQLTermNode.maybeQuote("a\"")); + } + + @Test + public void TestMaybeQuoteQuote2() { + assertEquals("a\\\"", CQLTermNode.maybeQuote("a\\\"")); + } + + @Test + public void TestMaybeQuoteQuote3() { + assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.maybeQuote("a" + "\\\\" + "\"")); + } + + @Test + public void TestMaybeQuoteQuote4() { + assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.maybeQuote("a" + "\\\\" + "\\\"")); + } + +} From 88f1a439044a8f6b157a9f71adf822cd57c7efec Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 3 Feb 2026 11:22:58 +0100 Subject: [PATCH 2/3] Rename --- .../java/org/z3950/zing/cql/CQLTermNode.java | 13 +++--- .../org/z3950/zing/cql/CQLTermNodeTest.java | 41 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/z3950/zing/cql/CQLTermNode.java b/src/main/java/org/z3950/zing/cql/CQLTermNode.java index 22c644f..39aac61 100644 --- a/src/main/java/org/z3950/zing/cql/CQLTermNode.java +++ b/src/main/java/org/z3950/zing/cql/CQLTermNode.java @@ -78,8 +78,8 @@ void toXCQLInternal(XCQLBuilder b, int level, List prefixes, @Override public String toCQL() { - String quotedIndex = maybeQuote(index); - String quotedTerm = maybeQuote(term); + String quotedIndex = toCQLTerm(index); + String quotedTerm = toCQLTerm(term); String res = quotedTerm; if (index != null && @@ -192,7 +192,7 @@ public String toPQF(Properties config) throws PQFTranslationException { if (isResultSetIndex(index)) { // Special case: ignore relation, modifiers, wildcards, etc. // There's parallel code in toType1BER() - return "@set " + maybeQuote(term); + return "@set " + toCQLTerm(term); } List attrs = getAttrs(config); @@ -219,10 +219,13 @@ public String toPQF(Properties config) throws PQFTranslationException { text = text.substring(0, len - 1); } - return s + maybeQuote(text); + return s + toCQLTerm(text); } - static String maybeQuote(String str) { + // ensure that a term is properly quoted for CQL output if necessary. + // If the term has a bare double-quote (") it will be + // escaped with a backslash. + static String toCQLTerm(String str) { if (str == null) { return null; } diff --git a/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java index 4bdda59..6c16ff6 100644 --- a/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java +++ b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java @@ -5,48 +5,53 @@ public class CQLTermNodeTest { @Test - public void TestMaybeQuoteNull() { - assertNull(CQLTermNode.maybeQuote(null)); + public void TestTCQLTermQuoteNull() { + assertNull(CQLTermNode.toCQLTerm(null)); } @Test - public void TestMaybeQuoteEmpty() { - assertEquals("\"\"", CQLTermNode.maybeQuote("")); + public void TestTCQLTermQuoteEmpty() { + assertEquals("\"\"", CQLTermNode.toCQLTerm("")); } @Test - public void TestMaybeQuoteRelation() { - assertEquals("\"<\"", CQLTermNode.maybeQuote("<")); + public void TestTCQLTermQuoteRelation() { + assertEquals("\"<\"", CQLTermNode.toCQLTerm("<")); } @Test - public void TestMaybeQuoteSimple() { - assertEquals("simple", CQLTermNode.maybeQuote("simple")); + public void TestTCQLTermQuoteSimple() { + assertEquals("simple", CQLTermNode.toCQLTerm("simple")); } @Test - public void TestMaybeQuoteBlank() { - assertEquals("\"a b\"", CQLTermNode.maybeQuote("a b")); + public void TestTCQLTermQuoteBlank() { + assertEquals("\"a b\"", CQLTermNode.toCQLTerm("a b")); } @Test - public void TestMaybeQuoteQuote1() { - assertEquals("a\\\"", CQLTermNode.maybeQuote("a\"")); + public void TestTCQLTermQuoteQuote1() { + assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\"")); } @Test - public void TestMaybeQuoteQuote2() { - assertEquals("a\\\"", CQLTermNode.maybeQuote("a\\\"")); + public void TestTCQLTermQuoteQuote2() { + assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\\\"")); } @Test - public void TestMaybeQuoteQuote3() { - assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.maybeQuote("a" + "\\\\" + "\"")); + public void TestTCQLTermQuoteQuote3() { + assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\"")); } @Test - public void TestMaybeQuoteQuote4() { - assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.maybeQuote("a" + "\\\\" + "\\\"")); + public void TestTCQLTermQuoteQuote4() { + assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\\\"")); + } + + @Test + public void TestTCQLTermQuoteBackSlashTrail() { + assertEquals("a\\", CQLTermNode.toCQLTerm("a\\")); } } From 2c843cdf3ce6786f447b5d34c3fcd789adf00079 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Tue, 3 Feb 2026 12:12:07 +0100 Subject: [PATCH 3/3] trailing backslash --- src/main/java/org/z3950/zing/cql/CQLTermNode.java | 4 ++++ src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/z3950/zing/cql/CQLTermNode.java b/src/main/java/org/z3950/zing/cql/CQLTermNode.java index 39aac61..13a26c8 100644 --- a/src/main/java/org/z3950/zing/cql/CQLTermNode.java +++ b/src/main/java/org/z3950/zing/cql/CQLTermNode.java @@ -242,6 +242,10 @@ static String toCQLTerm(String str) { escaped = ch == '\\' && !escaped; sb.append(ch); } + if (escaped) { + // trailing backslash - escape it + sb.append('\\'); + } if (quote) { return "\"" + sb.toString() + "\""; } else { diff --git a/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java index 6c16ff6..760c1fa 100644 --- a/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java +++ b/src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java @@ -50,8 +50,13 @@ public void TestTCQLTermQuoteQuote4() { } @Test - public void TestTCQLTermQuoteBackSlashTrail() { - assertEquals("a\\", CQLTermNode.toCQLTerm("a\\")); + public void TestTCQLTermQuoteBackSlashTrail1() { + assertEquals("a\\\\", CQLTermNode.toCQLTerm("a\\")); + } + + @Test + public void TestTCQLTermQuoteBackSlashTrail2() { + assertEquals("\"a \\\\\"", CQLTermNode.toCQLTerm("a \\")); } }