[Git][java-team/libpostgresql-jdbc-java][upstream] New upstream version 42.7.14

Christoph Berg (@myon) gitlab at salsa.debian.org
Fri Oct 9 10:29:20 BST 2026



Christoph Berg pushed to branch upstream at Debian Java Maintainers / libpostgresql-jdbc-java


Commits:
a87f3166 by Christoph Berg at 2026-10-09T11:14:23+02:00
New upstream version 42.7.14
- - - - -


11 changed files:

- pom.xml
- src/main/java/org/postgresql/core/AuthMethod.java
- src/main/java/org/postgresql/util/DriverInfo.java
- src/main/java/org/postgresql/util/internal/PgBufferedOutputStream.java
- src/main/resources/META-INF/MANIFEST.MF
- + src/test/java/org/postgresql/core/AuthMethodTest.java
- + src/test/java/org/postgresql/core/RequireAuthConnectTest.java
- + src/test/java/org/postgresql/core/RequireAuthValues.java
- src/test/java/org/postgresql/test/gss/GSSStreamTest.java
- src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java
- src/test/java/org/postgresql/util/internal/PgBufferedOutputStreamTest.java


Changes:

=====================================
pom.xml
=====================================
@@ -8,7 +8,7 @@
 
     <groupId>org.postgresql</groupId>
     <artifactId>postgresql</artifactId>
-    <version>42.7.13</version>
+    <version>42.7.14</version>
     <packaging>jar</packaging>
     <name>PostgreSQL JDBC Driver - JDBC 4.2</name>
     <description>Java JDBC 4.2 (JRE 8+) driver for PostgreSQL database</description>


=====================================
src/main/java/org/postgresql/core/AuthMethod.java
=====================================
@@ -31,17 +31,40 @@ public enum AuthMethod {
     }
   }
 
+  /**
+   * Parses a {@code requireAuth} value into the authentication methods the
+   * server may request.
+   *
+   * @param requireAuth comma-separated method names, either all plain or all
+   *     prefixed with {@code !}
+   * @return {@code null} when {@code requireAuth} is {@code null}, and
+   *     {@link #checkAuth} allows every method for {@code null}; otherwise the
+   *     allowed methods, empty when the value excludes every method
+   * @throws PSQLException with {@link PSQLState#INVALID_PARAMETER_VALUE} if
+   *     the value mixes the two forms, names a method twice, names an unknown
+   *     method, or is written without a method
+   */
   public static /* @Nullable */ EnumSet<AuthMethod> parseRequireAuth(/* @Nullable */ String requireAuth) throws PSQLException {
     if (requireAuth == null) {
       return null;
     }
 
-    EnumSet<AuthMethod> allowedMethods;
     EnumSet<AuthMethod> seenMethods = EnumSet.noneOf(AuthMethod.class);
     String[] methods = requireAuth.split(",");
-    boolean isDisallowMode = methods.length > 0 && methods[0].trim().startsWith("!");
+    // split drops empty strings at the end, so "," and ",," give an empty
+    // array. A value without a method in it names neither methods to allow
+    // nor methods to exclude. It is a mistake, not a request to refuse every
+    // method, so it fails as an invalid value.
+    if (methods.length == 0) {
+      throw new PSQLException(
+          GT.tr("Invalid authentication method: {0}", requireAuth),
+          PSQLState.INVALID_PARAMETER_VALUE);
+    }
+    boolean isDisallowMode = methods[0].trim().startsWith("!");
 
-    allowedMethods = isDisallowMode ? EnumSet.allOf(AuthMethod.class) : EnumSet.noneOf(AuthMethod.class);
+    EnumSet<AuthMethod> allowedMethods = isDisallowMode
+        ? EnumSet.allOf(AuthMethod.class)
+        : EnumSet.noneOf(AuthMethod.class);
 
     for (String method : methods) {
       method = method.trim();
@@ -62,9 +85,20 @@ public enum AuthMethod {
         allowedMethods.add(authMethod);
       }
     }
-    return allowedMethods.isEmpty() ? null : allowedMethods;
+    // Return an empty set, not null. checkAuth allows every method for null,
+    // so null here would let the server pick any method.
+    return allowedMethods;
   }
 
+  /**
+   * Refuses an authentication method that {@code requireAuth} does not allow.
+   *
+   * @param allowedMethods the methods to allow, or {@code null} to allow
+   *     every method; an empty set refuses every method
+   * @param authMethod the method the server asked for
+   * @throws PSQLException with {@link PSQLState#CONNECTION_REJECTED} if the
+   *     method is not allowed
+   */
   public static void checkAuth(/* @Nullable */ EnumSet<AuthMethod> allowedMethods, AuthMethod authMethod) throws PSQLException {
     if (allowedMethods == null) {
       return;


=====================================
src/main/java/org/postgresql/util/DriverInfo.java
=====================================
@@ -16,13 +16,13 @@ public final class DriverInfo {
   // Driver name
   public static final String DRIVER_NAME = "PostgreSQL JDBC Driver";
   public static final String DRIVER_SHORT_NAME = "PgJDBC";
-  public static final String DRIVER_VERSION = "42.7.13";
+  public static final String DRIVER_VERSION = "42.7.14";
   public static final String DRIVER_FULL_NAME = DRIVER_NAME + " " + DRIVER_VERSION;
 
   // Driver version
   public static final int MAJOR_VERSION = 42;
   public static final int MINOR_VERSION = 7;
-  public static final int PATCH_VERSION = 13;
+  public static final int PATCH_VERSION = 14;
 
   // JDBC specification
   public static final String JDBC_VERSION = "4.2";


=====================================
src/main/java/org/postgresql/util/internal/PgBufferedOutputStream.java
=====================================
@@ -164,31 +164,18 @@ public class PgBufferedOutputStream extends FilterOutputStream {
    * @throws IOException in case writing to the underlying stream fails
    */
   public void writeZeros(int len) throws IOException {
-    int startPos = count;
-    if (count > 0) {
-      int avail = buf.length - count;
-      int prefixLength = Math.min(len, avail);
-      Arrays.fill(buf, count, count + prefixLength, (byte) 0);
-      count += prefixLength;
-      len -= prefixLength;
+    if (len < 0) {
+      throw new IllegalArgumentException("len must be non-negative, got " + len);
+    }
+    // If the buffer is full on entry, the first pass fills nothing and just flushes.
+    do {
+      int chunk = Math.min(len, buf.length - count);
+      Arrays.fill(buf, count, count + chunk, (byte) 0);
+      count += chunk;
+      len -= chunk;
       if (count == buf.length) {
         flushBuffer();
       }
-      if (len == 0) {
-        return;
-      }
-    }
-    // The buffer is empty at this point, and startPos..buf.length is filled with zeros
-    // So fill the beginning with zeros as well.
-    Arrays.fill(buf, 0, Math.min(startPos, len), (byte) 0);
-
-    while (len >= buf.length) {
-      // Pretend we have the full buffer
-      count = buf.length;
-      flushBuffer();
-      len -= buf.length;
-    }
-    // Pretend we have the remaining zeros in the buffer.
-    count = len;
+    } while (len > 0);
   }
 }


=====================================
src/main/resources/META-INF/MANIFEST.MF
=====================================
@@ -1,7 +1,7 @@
 Manifest-Version: 1.0
 Bundle-License: BSD-2-Clause
 Implementation-Title: PostgreSQL JDBC Driver
-Implementation-Version: 42.7.13
+Implementation-Version: 42.7.14
 Specification-Vendor: Oracle Corporation
 Specification-Version: 4.2
 Specification-Title: JDBC


=====================================
src/test/java/org/postgresql/core/AuthMethodTest.java
=====================================
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.core;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.postgresql.util.PSQLException;
+import org.postgresql.util.PSQLState;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.EnumSet;
+
+/**
+ * An unset {@code requireAuth} allows every authentication method, and a
+ * value that excludes every method refuses all of them. A value cannot mix
+ * plain and {@code !} names, name a method twice, name an unknown method, or
+ * be written without a method. Commas at the very end of a value are
+ * ignored.
+ */
+class AuthMethodTest {
+
+  @Test
+  void anUnsetRequireAuthParsesToNull() throws PSQLException {
+    assertNull(AuthMethod.parseRequireAuth(null), "parseRequireAuth(null)");
+  }
+
+  @Test
+  void aPositiveListAllowsOnlyTheMethodsItNames() throws PSQLException {
+    assertEquals(EnumSet.of(AuthMethod.SCRAM_SHA_256, AuthMethod.GSS),
+        AuthMethod.parseRequireAuth("scram-sha-256,gss"));
+  }
+
+  @Test
+  void anExclusionListAllowsEveryMethodItDoesNotName() throws PSQLException {
+    EnumSet<AuthMethod> expected = EnumSet.allOf(AuthMethod.class);
+    expected.remove(AuthMethod.PASSWORD);
+    expected.remove(AuthMethod.MD5);
+
+    assertEquals(expected, AuthMethod.parseRequireAuth("!password,!md5"));
+  }
+
+  /**
+   * Excluding every method but one leaves exactly that one. Over all runs,
+   * each method is checked both when it is the only one allowed and when it
+   * is excluded. A method added to the enum later is checked too.
+   */
+  @ParameterizedTest(name = "every method but {0} excluded")
+  @EnumSource(AuthMethod.class)
+  void anExclusionListCanLeaveASingleMethod(AuthMethod kept)
+      throws PSQLException {
+    String allButKept =
+        RequireAuthValues.excluding(EnumSet.complementOf(EnumSet.of(kept)));
+    EnumSet<AuthMethod> allowed = AuthMethod.parseRequireAuth(allButKept);
+
+    assertEquals(EnumSet.of(kept), allowed,
+        "parseRequireAuth(\"" + allButKept + "\")");
+    assertDoesNotThrow(() -> AuthMethod.checkAuth(allowed, kept));
+    for (AuthMethod other : EnumSet.complementOf(EnumSet.of(kept))) {
+      PSQLException e = assertThrows(PSQLException.class,
+          () -> AuthMethod.checkAuth(allowed, other), "checkAuth for " + other);
+      assertEquals(PSQLState.CONNECTION_REJECTED.getState(), e.getSQLState(),
+          "SQLState of the refusal of " + other);
+    }
+  }
+
+  @Test
+  void surroundingSpaceIsIgnoredInAPositiveList() throws PSQLException {
+    assertEquals(EnumSet.of(AuthMethod.PASSWORD, AuthMethod.MD5),
+        AuthMethod.parseRequireAuth(" password , md5 "));
+  }
+
+  @Test
+  void surroundingSpaceIsIgnoredInAnExclusionList() throws PSQLException {
+    assertEquals(
+        EnumSet.complementOf(EnumSet.of(AuthMethod.PASSWORD, AuthMethod.MD5)),
+        AuthMethod.parseRequireAuth(" !password , !md5 "));
+  }
+
+  /**
+   * Excluding every method must parse to an empty set, not to null.
+   * checkAuth allows every method for null, so with null the driver sends a
+   * cleartext password to a server that asks for one.
+   */
+  @Test
+  void excludingEveryMethodParsesToAnEmptySet() throws PSQLException {
+    EnumSet<AuthMethod> allowed =
+        AuthMethod.parseRequireAuth(RequireAuthValues.EVERY_METHOD_EXCLUDED);
+
+    assertEquals(EnumSet.noneOf(AuthMethod.class), allowed,
+        "parseRequireAuth(\"" + RequireAuthValues.EVERY_METHOD_EXCLUDED
+            + "\")");
+  }
+
+  @ParameterizedTest(name = "{0} against an empty allowed set")
+  @EnumSource(AuthMethod.class)
+  void anEmptyAllowedSetRefusesEveryMethod(AuthMethod method) {
+    PSQLException e = assertThrows(PSQLException.class,
+        () -> AuthMethod.checkAuth(EnumSet.noneOf(AuthMethod.class), method));
+    assertEquals(PSQLState.CONNECTION_REJECTED.getState(), e.getSQLState(),
+        "SQLState of the refusal");
+  }
+
+  @ParameterizedTest(name = "{0} against an unset requireAuth")
+  @EnumSource(AuthMethod.class)
+  void anUnsetRequireAuthAllowsEveryMethod(AuthMethod method) {
+    assertDoesNotThrow(() -> AuthMethod.checkAuth(null, method));
+  }
+
+  @ParameterizedTest(name = "requireAuth={0}")
+  @ValueSource(strings = {"password,!md5", "!password,md5"})
+  void positiveAndNegativeFormsCannotBeMixed(String requireAuth) {
+    assertParseError(requireAuth);
+  }
+
+  @ParameterizedTest(name = "requireAuth={0}")
+  @ValueSource(strings = {"password,password", "!md5,!md5"})
+  void aMethodCannotBeNamedTwice(String requireAuth) {
+    assertParseError(requireAuth);
+  }
+
+  @ParameterizedTest(name = "requireAuth={0}")
+  @ValueSource(strings = {"sha-256", "trust"})
+  void anUnknownMethodIsRefused(String requireAuth) {
+    assertParseError(requireAuth);
+  }
+
+  /**
+   * Commas at the very end of the value are ignored, and the methods before
+   * them are enforced. Users may already have such a value in their
+   * connection settings, and refusing it would break their connections.
+   */
+  @ParameterizedTest(name = "requireAuth={0}")
+  @ValueSource(strings = {"password,", "password,,"})
+  void commasAtTheEndAreIgnored(String requireAuth) throws PSQLException {
+    assertEquals(EnumSet.of(AuthMethod.PASSWORD),
+        AuthMethod.parseRequireAuth(requireAuth),
+        "parseRequireAuth(\"" + requireAuth + "\")");
+  }
+
+  /**
+   * A value without a method in it is refused as an invalid value. It names
+   * neither methods to allow nor methods to exclude, so it is a mistake, not
+   * a request to refuse every method.
+   */
+  @ParameterizedTest(name = "requireAuth={0}")
+  @ValueSource(strings = {"!", ",", ",,"})
+  void aValueWithoutAMethodIsRefused(String requireAuth) {
+    assertParseError(requireAuth);
+  }
+
+  private static void assertParseError(String requireAuth) {
+    PSQLException e = assertThrows(PSQLException.class,
+        () -> AuthMethod.parseRequireAuth(requireAuth));
+    assertEquals(PSQLState.INVALID_PARAMETER_VALUE.getState(), e.getSQLState(),
+        "SQLState of the parse error");
+  }
+}


=====================================
src/test/java/org/postgresql/core/RequireAuthConnectTest.java
=====================================
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.core;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.postgresql.PGProperty;
+import org.postgresql.util.PSQLState;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.net.SocketFactory;
+
+/**
+ * A requireAuth value that excludes every method must refuse the connection
+ * with {@link PSQLState#CONNECTION_REJECTED}, whatever the server asks for,
+ * and the driver must not send the password.
+ */
+public class RequireAuthConnectTest {
+
+  /** AuthenticationCleartextPassword: the server asks for the password. */
+  private static final byte[] CLEARTEXT_PASSWORD_REQUEST =
+      {'R', 0, 0, 0, 8, 0, 0, 0, 3};
+
+  /** AuthenticationOk sent before any request: trust authentication. */
+  private static final byte[] AUTHENTICATION_OK =
+      {'R', 0, 0, 0, 8, 0, 0, 0, 0};
+
+  private static final String PASSWORD = "secret";
+
+  /** The PasswordMessage that carries {@link #PASSWORD}. */
+  private static final byte[] PASSWORD_MESSAGE =
+      {'p', 0, 0, 0, 11, 's', 'e', 'c', 'r', 'e', 't', 0};
+
+  @Test
+  void refusesACleartextPasswordRequest() {
+    FakeServer server = new FakeServer(CLEARTEXT_PASSWORD_REQUEST);
+    SQLException e = assertThrows(SQLException.class,
+        () -> server.connect(RequireAuthValues.EVERY_METHOD_EXCLUDED));
+    // The bytes are checked before the SQLState, so if the driver sends the
+    // password, the test fails on the password and not on the error that
+    // follows it.
+    assertArrayEquals(new byte[0], server.writtenAfterStartup(),
+        "bytes the driver wrote after the startup message");
+    assertEquals(PSQLState.CONNECTION_REJECTED.getState(), e.getSQLState(),
+        "SQLState of the refusal");
+  }
+
+  /**
+   * A server that sends the same request gets a PasswordMessage when
+   * requireAuth allows the password. Without this test, the empty result in
+   * {@link #refusesACleartextPasswordRequest} could come from a socket that
+   * stopped recording.
+   */
+  @Test
+  void sendsThePasswordWhenRequireAuthAllowsIt() {
+    FakeServer server = new FakeServer(CLEARTEXT_PASSWORD_REQUEST);
+    // The reply ends after the request, so the driver fails at end of
+    // stream once it has sent the password. This test does not check that
+    // failure.
+    assertThrows(SQLException.class, () -> server.connect("password"));
+    assertArrayEquals(PASSWORD_MESSAGE, server.writtenAfterStartup(),
+        "bytes the driver wrote after the startup message");
+  }
+
+  /**
+   * requireAuth calls trust authentication "none", and
+   * {@link RequireAuthValues#EVERY_METHOD_EXCLUDED} excludes "none" too.
+   */
+  @Test
+  void refusesTrust() {
+    FakeServer server = new FakeServer(AUTHENTICATION_OK);
+    SQLException e = assertThrows(SQLException.class,
+        () -> server.connect(RequireAuthValues.EVERY_METHOD_EXCLUDED));
+    assertEquals(PSQLState.CONNECTION_REJECTED.getState(), e.getSQLState(),
+        "SQLState of the refusal");
+  }
+
+  /** Replies with a fixed message and records what the driver writes. */
+  private static final class FakeServer {
+    /**
+     * The driver creates the socket factory from its class name and passes
+     * it only string properties. So the factory finds the server by the key
+     * that {@link #connect} passes as socketFactoryArg.
+     */
+    static final Map<String, FakeServer> BY_KEY = new ConcurrentHashMap<>();
+
+    final byte[] reply;
+    final ByteArrayOutputStream written = new ByteArrayOutputStream();
+
+    FakeServer(byte[] reply) {
+      this.reply = reply;
+    }
+
+    void connect(String requireAuth) throws SQLException {
+      String key = UUID.randomUUID().toString();
+      Properties props = new Properties();
+      PGProperty.USER.set(props, "test");
+      PGProperty.PASSWORD.set(props, PASSWORD);
+      PGProperty.SSL_MODE.set(props, "disable");
+      PGProperty.GSS_ENC_MODE.set(props, "disable");
+      PGProperty.REQUIRE_AUTH.set(props, requireAuth);
+      PGProperty.SOCKET_FACTORY.set(props,
+          FakeServerSocketFactory.class.getName());
+      PGProperty.SOCKET_FACTORY_ARG.set(props, key);
+      BY_KEY.put(key, this);
+      try {
+        // The factory ignores the host. The driver records the failure for the
+        // host in the JVM-wide GlobalHostStatusTracker, which multi-host URLs
+        // read, so the host is a name that cannot exist.
+        DriverManager.getConnection(
+            "jdbc:postgresql://requireauth.invalid/test", props).close();
+      } finally {
+        BY_KEY.remove(key);
+      }
+    }
+
+    /**
+     * Returns what the driver wrote after its startup message, which starts
+     * with its length.
+     */
+    byte[] writtenAfterStartup() {
+      byte[] bytes = written.toByteArray();
+      int startupLength = ByteBuffer.wrap(bytes).getInt();
+      return Arrays.copyOfRange(bytes, startupLength, bytes.length);
+    }
+  }
+
+  /**
+   * Creates sockets that read {@link FakeServer#reply} and write to
+   * {@link FakeServer#written}. The driver loads this class through the
+   * socketFactory property and calls only {@link #createSocket()}.
+   */
+  public static class FakeServerSocketFactory extends SocketFactory {
+    private final FakeServer server;
+
+    public FakeServerSocketFactory(String key) {
+      this.server = FakeServer.BY_KEY.get(key);
+    }
+
+    @Override
+    public Socket createSocket() {
+      return new Socket() {
+        private final InputStream in = new ByteArrayInputStream(server.reply);
+
+        /** Reports connected, so the driver does not connect it. */
+        @Override
+        public boolean isConnected() {
+          return true;
+        }
+
+        @Override
+        public InputStream getInputStream() {
+          return in;
+        }
+
+        @Override
+        public OutputStream getOutputStream() {
+          return server.written;
+        }
+      };
+    }
+
+    @Override
+    public Socket createSocket(String host, int port) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Socket createSocket(String host, int port, InetAddress localHost,
+        int localPort) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Socket createSocket(InetAddress host, int port) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Socket createSocket(InetAddress address, int port,
+        InetAddress localAddress, int localPort) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}


=====================================
src/test/java/org/postgresql/core/RequireAuthValues.java
=====================================
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.core;
+
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * Builds {@code requireAuth} values from the {@link AuthMethod} enum, so the
+ * tests pick up a new method without anyone editing a list. If a name built
+ * here does not match what {@link AuthMethod#fromString} accepts, fromString
+ * throws and the tests fail.
+ */
+final class RequireAuthValues {
+
+  /** Excludes every method, so a connection that uses it must be refused. */
+  static final String EVERY_METHOD_EXCLUDED =
+      excluding(EnumSet.allOf(AuthMethod.class));
+
+  private RequireAuthValues() {
+  }
+
+  /**
+   * The requireAuth value that excludes each of {@code methods}, as in
+   * {@code !password,!md5}.
+   */
+  static String excluding(EnumSet<AuthMethod> methods) {
+    return methods.stream()
+        .map(method ->
+            "!" + method.name().toLowerCase(Locale.ROOT).replace('_', '-'))
+        .collect(Collectors.joining(","));
+  }
+}


=====================================
src/test/java/org/postgresql/test/gss/GSSStreamTest.java
=====================================
@@ -18,6 +18,8 @@ import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
 import java.util.Random;
 
 public class GSSStreamTest {
@@ -74,4 +76,44 @@ public class GSSStreamTest {
     assertArrayEquals(testMessage, unwrapResult,
         "the message should be intact after wrap and unwrap");
   }
+
+  /**
+   * {@link GSSOutputStream} inherits {@code writeZeros}. {@code write} sends 100 bytes or more
+   * written into an empty buffer straight from the caller's array, so the data goes in as 3 bytes
+   * and then 97: the 97-byte write fills the buffer exactly, {@code write} sends it and leaves the
+   * 0xff bytes in the array, and {@code writeZeros} starts on an empty buffer that is not zeroed.
+   * It used to send those bytes in place of the zeros.
+   */
+  @Test
+  void writeZerosAfterDataEndingAtBufferBoundarySendsZeros() throws Exception {
+    ByteArrayOutputStream wrappedContents = new ByteArrayOutputStream();
+    MockGSSContext gssContext = new MockGSSContext(0, messageProp);
+    // MockGSSContext.getWrapSizeLimit returns 100, so the GSSOutputStream buffer holds 100 bytes
+    GSSOutputStream out = new GSSOutputStream(
+        new PgBufferedOutputStream(wrappedContents, 20), gssContext, messageProp, 20);
+    byte[] data = new byte[100];
+    Arrays.fill(data, (byte) 0xff);
+    out.write(data, 0, 3);
+    out.write(data, 3, 97);
+
+    out.writeZeros(103);
+    out.flush();
+
+    byte[] expected = new byte[203];
+    Arrays.fill(expected, 0, 100, (byte) 0xff);
+    assertArrayEquals(expected, unwrap(wrappedContents.toByteArray(), gssContext),
+        "100 bytes of 0xff written as 3 + 97, then writeZeros(103)");
+  }
+
+  private byte[] unwrap(byte[] wrapped, MockGSSContext gssContext) throws IOException {
+    GSSInputStream in =
+        new GSSInputStream(new ByteArrayInputStream(wrapped), gssContext, messageProp);
+    ByteArrayOutputStream unwrapped = new ByteArrayOutputStream();
+    byte[] tmp = new byte[256];
+    int n;
+    while ((n = in.read(tmp)) != -1) {
+      unwrapped.write(tmp, 0, n);
+    }
+    return unwrapped.toByteArray();
+  }
 }


=====================================
src/test/java/org/postgresql/test/util/ByteStreamWriterTest.java
=====================================
@@ -176,6 +176,22 @@ public class ByteStreamWriterTest extends BaseTest4 {
     validateContent(expectedData);
   }
 
+  /**
+   * The content is more than twice the 8192-byte send buffer, so the driver sends it to the socket
+   * without copying it into the buffer, and the buffer array still holds the bytes of the messages
+   * sent before the value when the padding starts. Those bytes used to be sent in place of the
+   * zeros, and the server stored them in the column.
+   */
+  @Test
+  public void contentOverTwiceTheSendBufferIsPaddedWithZeros() throws Exception {
+    ByteBuffer testData = testData(20 * 1024);
+    insertStream(testData, 30 * 1024);
+    byte[] expectedData = new byte[30 * 1024];
+    testData.rewind();
+    testData.get(expectedData, 0, 20 * 1024);
+    validateContent(expectedData);
+  }
+
   @Test
   public void testLengthLessThanContent() throws Exception {
     ByteBuffer testData = testData(8);


=====================================
src/test/java/org/postgresql/util/internal/PgBufferedOutputStreamTest.java
=====================================
@@ -6,6 +6,7 @@
 package org.postgresql.util.internal;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.params.provider.Arguments.arguments;
@@ -290,6 +291,32 @@ public class PgBufferedOutputStreamTest {
           }
       );
     }
+
+    /**
+     * {@code flush()} sends the buffer and resets its count, but leaves the 0xff bytes in the
+     * array, so {@code writeZeros} starts on an empty buffer that is not zeroed. It used to send
+     * those bytes in place of the zeros.
+     */
+    @ParameterizedTest
+    @ValueSource(ints = {1, ZEROS_BUFFER_SIZE - 1, ZEROS_BUFFER_SIZE, ZEROS_BUFFER_SIZE + 1,
+        ZEROS_BUFFER_SIZE * 2 + 1})
+    void writesZerosWhenTheFlushedBufferStillHoldsEarlierBytes(int numZeros) throws IOException {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      int bufferSize = ZEROS_BUFFER_SIZE;
+      PgBufferedOutputStream out = new PgBufferedOutputStream(baos, bufferSize);
+      for (int i = 0; i < bufferSize; i++) {
+        out.write(0xff);
+      }
+      out.flush();
+
+      out.writeZeros(numZeros);
+      out.flush();
+
+      byte[] expected = new byte[bufferSize + numZeros];
+      Arrays.fill(expected, 0, bufferSize, (byte) 0xff);
+      assertArrayEquals(expected, baos.toByteArray(),
+          () -> bufferSize + " bytes of 0xff, flush(), writeZeros(" + numZeros + ")");
+    }
   }
 
   @Test



View it on GitLab: https://salsa.debian.org/java-team/libpostgresql-jdbc-java/-/commit/a87f3166bb93eafd1368c34299190366996ef902

-- 
View it on GitLab: https://salsa.debian.org/java-team/libpostgresql-jdbc-java/-/commit/a87f3166bb93eafd1368c34299190366996ef902
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-java-commits/attachments/20261009/7eaea77e/attachment.htm>


More information about the pkg-java-commits mailing list