Skip to content

Commit 8d2feee

Browse files
committed
Harden JassDoc database replacement
1 parent 9b310ae commit 8d2feee

2 files changed

Lines changed: 109 additions & 17 deletions

File tree

de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/JassDocService.java

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@
3434
import java.util.ArrayList;
3535
import java.util.Base64;
3636
import java.util.Comparator;
37+
import java.util.HashSet;
3738
import java.util.LinkedHashMap;
3839
import java.util.List;
3940
import java.util.Locale;
4041
import java.util.Map;
4142
import java.util.Optional;
43+
import java.util.Set;
4244
import java.util.concurrent.ConcurrentHashMap;
4345
import java.util.concurrent.atomic.AtomicBoolean;
4446
import java.util.function.Function;
@@ -257,7 +259,7 @@ private Optional<String> lookupDocumentation(LookupKey key) {
257259
}
258260
try (Connection conn = open(dbPath.get())) {
259261
List<TableSchema> schemas = discoverSchemas(conn);
260-
boolean hasLegacySchema = hasLegacyJassdocSchema(conn);
262+
boolean hasLegacySchema = hasCompatibleLegacyJassdocSchema(conn);
261263
if (schemas.isEmpty() && !hasLegacySchema) {
262264
WLogger.warning("JassDoc DB found, but no compatible documentation tables were detected.");
263265
initFailed = true;
@@ -365,7 +367,7 @@ private void triggerAsyncInit() {
365367
}
366368

367369
private @Nullable String lookupFromLegacyJassdocTables(Connection conn, LookupKey key) throws SQLException {
368-
if (!tableExists(conn, "parameters")) {
370+
if (!hasCompatibleLegacyJassdocSchema(conn)) {
369371
return null;
370372
}
371373
Map<String, String> params = readKeyValueRows(conn, "parameters", "fnname", "param", "value", key.symbolName());
@@ -778,17 +780,36 @@ void installDownloadedDatabase(Path downloaded, Path target) throws IOException
778780
try {
779781
replaceAtomically(downloaded, target);
780782
} catch (IOException installFailure) {
781-
if (!Files.exists(target) && Files.exists(backup)) {
782-
Files.copy(backup, target, StandardCopyOption.REPLACE_EXISTING,
783-
StandardCopyOption.COPY_ATTRIBUTES);
783+
if (Files.exists(backup)) {
784+
restoreBackup(backup, target, installFailure);
784785
}
785786
throw installFailure;
786787
}
787788
}
788789

790+
void restoreBackup(Path backup, Path target, IOException installFailure) {
791+
Path restoreTmp = null;
792+
try {
793+
restoreTmp = Files.createTempFile(target.getParent(), "jassdoc-restore-", ".tmp");
794+
Files.copy(backup, restoreTmp, StandardCopyOption.REPLACE_EXISTING,
795+
StandardCopyOption.COPY_ATTRIBUTES);
796+
replaceAtomically(restoreTmp, target);
797+
} catch (IOException restoreFailure) {
798+
installFailure.addSuppressed(restoreFailure);
799+
} finally {
800+
if (restoreTmp != null) {
801+
try {
802+
Files.deleteIfExists(restoreTmp);
803+
} catch (IOException cleanupFailure) {
804+
installFailure.addSuppressed(cleanupFailure);
805+
}
806+
}
807+
}
808+
}
809+
789810
private void validateDownloadedDatabase(Path downloaded) throws IOException {
790811
try (Connection conn = open(downloaded)) {
791-
if (discoverSchemas(conn).isEmpty() && !hasLegacyJassdocSchema(conn)) {
812+
if (discoverSchemas(conn).isEmpty() && !hasCompatibleLegacyJassdocSchema(conn)) {
792813
throw new IOException("Downloaded file is not a compatible JassDoc database");
793814
}
794815
} catch (SQLException e) {
@@ -818,7 +839,19 @@ private HttpURLConnection openHttpConnection(URL url) throws IOException {
818839
return (HttpURLConnection) url.openConnection();
819840
}
820841

821-
String value = proxySetting.get();
842+
URI proxyUri = parseHttpProxyUri(proxySetting.get());
843+
int port = proxyUri.getPort() >= 0 ? proxyUri.getPort() : 80;
844+
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUri.getHost(), port));
845+
HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
846+
if (proxyUri.getUserInfo() != null) {
847+
String credentials = Base64.getEncoder().encodeToString(
848+
proxyUri.getUserInfo().getBytes(StandardCharsets.UTF_8));
849+
connection.setRequestProperty("Proxy-Authorization", "Basic " + credentials);
850+
}
851+
return connection;
852+
}
853+
854+
static URI parseHttpProxyUri(String value) throws IOException {
822855
URI proxyUri;
823856
try {
824857
proxyUri = URI.create(value.contains("://") ? value : "http://" + value);
@@ -828,15 +861,11 @@ private HttpURLConnection openHttpConnection(URL url) throws IOException {
828861
if (proxyUri.getHost() == null) {
829862
throw new IOException("Invalid JassDoc proxy URL: missing host");
830863
}
831-
int port = proxyUri.getPort() >= 0 ? proxyUri.getPort() : 80;
832-
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyUri.getHost(), port));
833-
HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
834-
if (proxyUri.getUserInfo() != null) {
835-
String credentials = Base64.getEncoder().encodeToString(
836-
proxyUri.getUserInfo().getBytes(StandardCharsets.UTF_8));
837-
connection.setRequestProperty("Proxy-Authorization", "Basic " + credentials);
864+
if (!"http".equalsIgnoreCase(proxyUri.getScheme())) {
865+
throw new IOException("Unsupported JassDoc proxy scheme '" + proxyUri.getScheme()
866+
+ "'; use an http:// proxy URL");
838867
}
839-
return connection;
868+
return proxyUri;
840869
}
841870

842871
static boolean shouldBypassProxy(String host, String noProxySetting) {
@@ -913,8 +942,32 @@ private List<TableSchema> discoverSchemas(Connection conn) throws SQLException {
913942
return result;
914943
}
915944

916-
private boolean hasLegacyJassdocSchema(Connection conn) throws SQLException {
917-
return tableExists(conn, "parameters");
945+
private boolean hasCompatibleLegacyJassdocSchema(Connection conn) throws SQLException {
946+
return tableHasColumns(conn, "parameters", "fnname", "param", "value")
947+
&& (!tableExists(conn, "annotations")
948+
|| tableHasColumns(conn, "annotations", "fnname", "anname", "value"))
949+
&& (!tableExists(conn, "params_extra")
950+
|| tableHasColumns(conn, "params_extra", "fnname", "param", "anname", "value"));
951+
}
952+
953+
private boolean tableHasColumns(Connection conn, String tableName, String... requiredColumns)
954+
throws SQLException {
955+
Set<String> columns = new HashSet<>();
956+
DatabaseMetaData md = conn.getMetaData();
957+
try (ResultSet result = md.getColumns(null, null, tableName, "%")) {
958+
while (result.next()) {
959+
String name = result.getString("COLUMN_NAME");
960+
if (name != null) {
961+
columns.add(name.toLowerCase(Locale.ROOT));
962+
}
963+
}
964+
}
965+
for (String required : requiredColumns) {
966+
if (!columns.contains(required)) {
967+
return false;
968+
}
969+
}
970+
return true;
918971
}
919972

920973
private boolean tableExists(Connection conn, String tableName) throws SQLException {

de.peeeq.wurstscript/src/test/java/de/peeeq/wurstio/languageserver/JassDocServiceTests.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import static org.testng.Assert.assertFalse;
1616
import static org.testng.Assert.assertThrows;
1717
import static org.testng.Assert.assertTrue;
18+
import static org.testng.Assert.expectThrows;
1819

1920
public class JassDocServiceTests {
2021

@@ -57,6 +58,37 @@ public void successfulUpdateKeepsPreviousDatabaseBackup() throws Exception {
5758
}
5859
}
5960

61+
@Test
62+
public void incompleteLegacySchemaDoesNotReplaceExistingDatabase() throws Exception {
63+
Path dir = Files.createTempDirectory("jassdoc-incomplete-legacy-");
64+
Path target = dir.resolve("jassdoc-latest.db");
65+
Path downloaded = dir.resolve("download.tmp");
66+
Files.writeString(target, "working database", StandardCharsets.UTF_8);
67+
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:" + downloaded.toAbsolutePath());
68+
Statement statement = conn.createStatement()) {
69+
statement.execute("CREATE TABLE parameters(fnname TEXT)");
70+
}
71+
72+
assertThrows(IOException.class,
73+
() -> new JassDocService().installDownloadedDatabase(downloaded, target));
74+
assertEquals(Files.readString(target, StandardCharsets.UTF_8), "working database");
75+
}
76+
77+
@Test
78+
public void restoreOverwritesAResidualPartialTarget() throws IOException {
79+
Path dir = Files.createTempDirectory("jassdoc-restore-");
80+
Path target = dir.resolve("jassdoc-latest.db");
81+
Path backup = dir.resolve("jassdoc-latest.db.bak");
82+
Files.writeString(target, "partial replacement", StandardCharsets.UTF_8);
83+
Files.writeString(backup, "working database", StandardCharsets.UTF_8);
84+
IOException installFailure = new IOException("simulated interrupted replacement");
85+
86+
new JassDocService().restoreBackup(backup, target, installFailure);
87+
88+
assertEquals(Files.readString(target, StandardCharsets.UTF_8), "working database");
89+
assertEquals(installFailure.getSuppressed().length, 0);
90+
}
91+
6092
@Test
6193
public void automaticUpdatesCanBeDisabled() {
6294
String previous = System.getProperty("WURST_JASSDOC_DB_AUTO_UPDATE");
@@ -83,4 +115,11 @@ public void proxyBypassSupportsStandardHostForms() {
83115
assertFalse(JassDocService.shouldBypassProxy(
84116
"github.com", "localhost, example.com"));
85117
}
118+
119+
@Test
120+
public void unsupportedTlsProxyIsRejectedExplicitly() {
121+
IOException error = expectThrows(IOException.class,
122+
() -> JassDocService.parseHttpProxyUri("https://proxy.example"));
123+
assertTrue(error.getMessage().contains("use an http:// proxy URL"));
124+
}
86125
}

0 commit comments

Comments
 (0)