diff --git a/src/main/java/org/lmdbjava/BufferProxy.java b/src/main/java/org/lmdbjava/BufferProxy.java index aff732b..f42da7f 100644 --- a/src/main/java/org/lmdbjava/BufferProxy.java +++ b/src/main/java/org/lmdbjava/BufferProxy.java @@ -21,6 +21,10 @@ package org.lmdbjava; import static java.lang.Long.BYTES; +import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; +import static org.lmdbjava.DbiFlags.MDB_UNSIGNEDKEY; +import static org.lmdbjava.MaskedFlag.isSet; +import static org.lmdbjava.MaskedFlag.mask; import java.util.Comparator; @@ -72,8 +76,24 @@ public abstract class BufferProxy { * @param flags for the database * @return a comparator that can be used (never null) */ - protected abstract Comparator getComparator(DbiFlags... flags); + protected Comparator getComparator(DbiFlags... flags) { + final int intFlag = mask(flags); + return isSet(intFlag, MDB_INTEGERKEY) || isSet(intFlag, MDB_UNSIGNEDKEY) ? getUnsignedComparator() : getSignedComparator(); + } + + /** + * Get a suitable default {@link Comparator} to compare numeric key values as unsigned. + * + * @return a comparator that can be used (never null) + */ + protected abstract Comparator getUnsignedComparator(); + /** + * Get a suitable default {@link Comparator} to compare numeric key values as signed. + * + * @return a comparator that can be used (never null) + */ + protected abstract Comparator getSignedComparator(); /** * Deallocate a buffer that was previously provided by {@link #allocate()}. * diff --git a/src/main/java/org/lmdbjava/ByteArrayProxy.java b/src/main/java/org/lmdbjava/ByteArrayProxy.java index 3fe8184..2066b67 100644 --- a/src/main/java/org/lmdbjava/ByteArrayProxy.java +++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java @@ -20,14 +20,15 @@ package org.lmdbjava; -import static java.util.Objects.requireNonNull; -import static org.lmdbjava.Library.RUNTIME; +import jnr.ffi.Pointer; +import jnr.ffi.provider.MemoryManager; import java.util.Arrays; import java.util.Comparator; -import jnr.ffi.Pointer; -import jnr.ffi.provider.MemoryManager; +import static java.lang.Math.min; +import static java.util.Objects.requireNonNull; +import static org.lmdbjava.Library.RUNTIME; /** * Byte array proxy. @@ -43,7 +44,10 @@ public final class ByteArrayProxy extends BufferProxy { private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); - private ByteArrayProxy() { + private static final Comparator signedComparator = ByteArrayProxy::compareArraysSigned; + private static final Comparator unsignedComparator = ByteArrayProxy::compareArrays; + + private ByteArrayProxy() { } /** @@ -60,7 +64,7 @@ public static int compareArrays(final byte[] o1, final byte[] o2) { if (o1 == o2) { return 0; } - final int minLength = Math.min(o1.length, o2.length); + final int minLength = min(o1.length, o2.length); for (int i = 0; i < minLength; i++) { final int lw = Byte.toUnsignedInt(o1[i]); @@ -74,15 +78,32 @@ public static int compareArrays(final byte[] o1, final byte[] o2) { return o1.length - o2.length; } + /** + * Compare two byte arrays. + * + * @param b1 left operand (required) + * @param b2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + @SuppressWarnings("PMD.CompareObjectsWithEquals") + public static int compareArraysSigned(final byte[] b1, final byte[] b2) { + requireNonNull(b1); + requireNonNull(b2); + + if (b1 == b2) return 0; + + for(int i = 0; i < min(b1.length, b2.length); ++i) { + if(b1[i] != b2[i]) return b1[i] - b2[i]; + } + + return b1.length - b2.length; + } + @Override protected byte[] allocate() { return new byte[0]; } - protected int compare(final byte[] o1, final byte[] o2) { - return compareArrays(o1, o2); - } - @Override protected void deallocate(final byte[] buff) { // byte arrays cannot be allocated @@ -94,8 +115,13 @@ protected byte[] getBytes(final byte[] buffer) { } @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; + protected Comparator getSignedComparator() { + return signedComparator; + } + + @Override + protected Comparator getUnsignedComparator() { + return unsignedComparator; } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java index 2635167..ed3d71e 100644 --- a/src/main/java/org/lmdbjava/ByteBufProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufProxy.java @@ -20,16 +20,17 @@ package org.lmdbjava; -import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; -import static java.lang.Class.forName; -import static org.lmdbjava.UnsafeAccess.UNSAFE; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import jnr.ffi.Pointer; import java.lang.reflect.Field; import java.util.Comparator; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.PooledByteBufAllocator; -import jnr.ffi.Pointer; +import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; +import static java.lang.Class.forName; +import static java.util.Objects.requireNonNull; +import static org.lmdbjava.UnsafeAccess.UNSAFE; /** * A buffer proxy backed by Netty's {@link ByteBuf}. @@ -51,6 +52,12 @@ public final class ByteBufProxy extends BufferProxy { private static final String FIELD_NAME_ADDRESS = "memoryAddress"; private static final String FIELD_NAME_LENGTH = "length"; private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf"; + private static final Comparator comparator = (o1, o2) -> { + requireNonNull(o1); + requireNonNull(o2); + + return o1.compareTo(o2); + }; private final long lengthOffset; private final long addressOffset; @@ -107,13 +114,14 @@ protected ByteBuf allocate() { throw new IllegalStateException("Netty buffer must be " + NAME); } - protected int compare(final ByteBuf o1, final ByteBuf o2) { - return o1.compareTo(o2); + @Override + protected Comparator getSignedComparator() { + return comparator; } @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; + protected Comparator getUnsignedComparator() { + return comparator; } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java index 8eb95da..9983d76 100644 --- a/src/main/java/org/lmdbjava/ByteBufferProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java @@ -27,6 +27,7 @@ import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.util.Objects.requireNonNull; import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; +import static org.lmdbjava.DbiFlags.MDB_UNSIGNEDKEY; import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.MaskedFlag.isSet; import static org.lmdbjava.MaskedFlag.mask; @@ -111,6 +112,14 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { protected static final String FIELD_NAME_ADDRESS = "address"; protected static final String FIELD_NAME_CAPACITY = "capacity"; + private static final Comparator signedComparator = (o1, o2) -> { + requireNonNull(o1); + requireNonNull(o2); + + return o1.compareTo(o2); + }; + private static final Comparator unsignedComparator = AbstractByteBufferProxy::compareBuff; + /** * A thread-safe pool for a given length. If the buffer found is valid (ie * not of a negative length) then that buffer is used. If no valid buffer is @@ -193,22 +202,13 @@ protected final ByteBuffer allocate() { } @Override - protected Comparator getComparator(final DbiFlags... flags) { - final int flagInt = mask(flags); - if (isSet(flagInt, MDB_INTEGERKEY)) { - return this::compareCustom; - } - return this::compareDefault; + protected Comparator getSignedComparator() { + return signedComparator; } - protected final int compareDefault(final ByteBuffer o1, - final ByteBuffer o2) { - return o1.compareTo(o2); - } - - protected final int compareCustom(final ByteBuffer o1, - final ByteBuffer o2) { - return compareBuff(o1, o2); + @Override + protected Comparator getUnsignedComparator() { + return unsignedComparator; } @Override diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index ed7c184..cb9130e 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -40,6 +40,8 @@ import jnr.ffi.Pointer; import jnr.ffi.byref.NativeLongByReference; +import java.util.Arrays; + /** * A cursor handle. * @@ -120,7 +122,7 @@ public void delete(final PutFlags... f) { txn.checkReady(); txn.checkWritesAllowed(); } - final int flags = mask(f); + final int flags = mask(true, f); checkRc(LIB.mdb_cursor_del(ptrCursor, flags)); } @@ -256,7 +258,7 @@ public boolean put(final T key, final T val, final PutFlags... op) { } kv.keyIn(key); kv.valIn(val); - final int mask = mask(op); + final int mask = mask(true, op); final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), mask); if (rc == MDB_KEYEXIST) { @@ -299,7 +301,7 @@ public void putMultiple(final T key, final T val, final int elements, txn.checkReady(); txn.checkWritesAllowed(); } - final int mask = mask(op); + final int mask = mask(true, op); if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) { throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); } @@ -364,7 +366,7 @@ public T reserve(final T key, final int size, final PutFlags... op) { } kv.keyIn(key); kv.valIn(size); - final int flags = mask(op) | MDB_RESERVE.getMask(); + final int flags = mask(true, op) | MDB_RESERVE.getMask(); checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags)); kv.valOut(); diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index ef8ec31..c5be4f8 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -20,6 +20,17 @@ package org.lmdbjava; +import jnr.ffi.Pointer; +import jnr.ffi.byref.IntByReference; +import jnr.ffi.byref.PointerByReference; +import org.lmdbjava.Library.ComparatorCallback; +import org.lmdbjava.Library.MDB_stat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + import static java.util.Objects.requireNonNull; import static jnr.ffi.Memory.allocateDirect; import static jnr.ffi.NativeType.ADDRESS; @@ -36,17 +47,6 @@ import static org.lmdbjava.PutFlags.MDB_RESERVE; import static org.lmdbjava.ResultCodeMapper.checkRc; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - -import jnr.ffi.Pointer; -import jnr.ffi.byref.IntByReference; -import jnr.ffi.byref.PointerByReference; -import org.lmdbjava.Library.ComparatorCallback; -import org.lmdbjava.Library.MDB_stat; - /** * LMDB Database. * @@ -64,10 +64,18 @@ public final class Dbi { Dbi(final Env env, final Txn txn, final byte[] name, final Comparator comparator, final boolean nativeCb, final BufferProxy proxy, final DbiFlags... flags) { + if (SHOULD_CHECK) { + requireNonNull(txn); + txn.checkReady(); + } this.env = env; this.name = name == null ? null : Arrays.copyOf(name, name.length); - this.comparator = comparator; - final int flagsMask = mask(flags); + if(comparator == null) { + this.comparator = proxy.getComparator(flags); + } else { + this.comparator = comparator; + } + final int flagsMask = mask(true, flags); final Pointer dbiPtr = allocateDirect(RUNTIME, ADDRESS); checkRc(LIB.mdb_dbi_open(txn.pointer(), name, flagsMask, dbiPtr)); ptr = dbiPtr.getPointer(0); @@ -377,7 +385,7 @@ public boolean put(final Txn txn, final T key, final T val, } txn.kv().keyIn(key); txn.kv().valIn(val); - final int mask = mask(flags); + final int mask = mask(true, flags); final int rc = LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn .kv().pointerVal(), mask); if (rc == MDB_KEYEXIST) { @@ -422,7 +430,7 @@ public T reserve(final Txn txn, final T key, final int size, } txn.kv().keyIn(key); txn.kv().valIn(size); - final int flags = mask(op) | MDB_RESERVE.getMask(); + final int flags = mask(true, op) | MDB_RESERVE.getMask(); checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv() .pointerVal(), flags)); txn.kv().valOut(); // marked as in,out in LMDB C docs diff --git a/src/main/java/org/lmdbjava/DbiFlags.java b/src/main/java/org/lmdbjava/DbiFlags.java index 081fb80..7277b55 100644 --- a/src/main/java/org/lmdbjava/DbiFlags.java +++ b/src/main/java/org/lmdbjava/DbiFlags.java @@ -68,6 +68,15 @@ public enum DbiFlags implements MaskedFlag { * similar to {@link #MDB_INTEGERKEY} keys. */ MDB_INTEGERDUP(0x20), + /** + * Compare the numeric keys in native byte order and as unsigned. + * + *

+ * This option is applied only to {@link java.nio.ByteBuffer}, {@link org.agrona.DirectBuffer} and byte array keys. + * {@link io.netty.buffer.ByteBuf} keys are always compared in native byte order and as unsigned. + *

+ */ + MDB_UNSIGNEDKEY(0x30, false), /** * With {@link #MDB_DUPSORT}, use reverse string dups. * @@ -86,9 +95,15 @@ public enum DbiFlags implements MaskedFlag { MDB_CREATE(0x4_0000); private final int mask; + private final boolean propagatedToLmdb; - DbiFlags(final int mask) { + DbiFlags(final int mask, final boolean propagatedToLmdb) { this.mask = mask; + this.propagatedToLmdb = propagatedToLmdb; + } + + DbiFlags(final int mask) { + this(mask, true); } @Override @@ -96,4 +111,8 @@ public int getMask() { return mask; } + @Override + public boolean isPropagatedToLmdb() { + return propagatedToLmdb; + } } diff --git a/src/main/java/org/lmdbjava/DirectBufferProxy.java b/src/main/java/org/lmdbjava/DirectBufferProxy.java index 62b4909..3bde81e 100644 --- a/src/main/java/org/lmdbjava/DirectBufferProxy.java +++ b/src/main/java/org/lmdbjava/DirectBufferProxy.java @@ -20,20 +20,20 @@ package org.lmdbjava; -import static java.lang.ThreadLocal.withInitial; -import static java.nio.ByteBuffer.allocateDirect; -import static java.nio.ByteOrder.BIG_ENDIAN; -import static java.util.Objects.requireNonNull; -import static org.lmdbjava.UnsafeAccess.UNSAFE; +import jnr.ffi.Pointer; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.Comparator; -import jnr.ffi.Pointer; -import org.agrona.DirectBuffer; -import org.agrona.MutableDirectBuffer; -import org.agrona.concurrent.UnsafeBuffer; +import static java.lang.ThreadLocal.withInitial; +import static java.nio.ByteBuffer.allocateDirect; +import static java.nio.ByteOrder.BIG_ENDIAN; +import static java.util.Objects.requireNonNull; +import static org.lmdbjava.UnsafeAccess.UNSAFE; /** * A buffer proxy backed by Agrona's {@link DirectBuffer}. @@ -42,6 +42,13 @@ * This class requires {@link UnsafeAccess} and Agrona must be in the classpath. */ public final class DirectBufferProxy extends BufferProxy { + private static final Comparator signedComparator = (o1, o2) -> { + requireNonNull(o1); + requireNonNull(o2); + + return o1.compareTo(o2); + }; + private static final Comparator unsignedComparator = DirectBufferProxy::compareBuff; /** * The {@link MutableDirectBuffer} proxy. Guaranteed to never be null, @@ -112,8 +119,14 @@ protected DirectBuffer allocate() { } } - protected int compare(final DirectBuffer o1, final DirectBuffer o2) { - return compareBuff(o1, o2); + @Override + protected Comparator getSignedComparator() { + return signedComparator; + } + + @Override + protected Comparator getUnsignedComparator() { + return unsignedComparator; } @Override @@ -129,11 +142,6 @@ protected byte[] getBytes(final DirectBuffer buffer) { return dest; } - @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; - } - @Override protected void in(final DirectBuffer buffer, final Pointer ptr, final long ptrAddr) { diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index db8b0f4..ae7e46c 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -20,6 +20,19 @@ package org.lmdbjava; +import jnr.ffi.Pointer; +import jnr.ffi.byref.IntByReference; +import jnr.ffi.byref.PointerByReference; +import org.lmdbjava.Library.MDB_envinfo; +import org.lmdbjava.Library.MDB_stat; + +import java.io.File; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + import static java.lang.Boolean.getBoolean; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; @@ -33,19 +46,6 @@ import static org.lmdbjava.ResultCodeMapper.checkRc; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; -import java.io.File; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import jnr.ffi.Pointer; -import jnr.ffi.byref.IntByReference; -import jnr.ffi.byref.PointerByReference; -import org.lmdbjava.Library.MDB_envinfo; -import org.lmdbjava.Library.MDB_stat; - /** * LMDB environment. * @@ -159,7 +159,7 @@ public void close() { public void copy(final File path, final CopyFlags... flags) { requireNonNull(path); validatePath(path); - final int flagsMask = mask(flags); + final int flagsMask = mask(true, flags); checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flagsMask)); } @@ -388,17 +388,7 @@ public Dbi openDbi(final byte[] name, final Comparator comparator, public Dbi openDbi(final Txn txn, final byte[] name, final Comparator comparator, final boolean nativeCb, final DbiFlags... flags) { - if (SHOULD_CHECK) { - requireNonNull(txn); - txn.checkReady(); - } - final Comparator useComparator; - if (comparator == null) { - useComparator = proxy.getComparator(flags); - } else { - useComparator = comparator; - } - return new Dbi<>(this, txn, name, useComparator, nativeCb, proxy, flags); + return new Dbi<>(this, txn, name, comparator, nativeCb, proxy, flags); } /** @@ -583,7 +573,7 @@ public Env open(final File path, final int mode, checkRc(LIB.mdb_env_set_mapsize(ptr, mapSize)); checkRc(LIB.mdb_env_set_maxdbs(ptr, maxDbs)); checkRc(LIB.mdb_env_set_maxreaders(ptr, maxReaders)); - final int flagsMask = mask(flags); + final int flagsMask = mask(true, flags); final boolean readOnly = isSet(flagsMask, MDB_RDONLY_ENV); final boolean noSubDir = isSet(flagsMask, MDB_NOSUBDIR); checkRc(LIB.mdb_env_open(ptr, path.getAbsolutePath(), flagsMask, mode)); diff --git a/src/main/java/org/lmdbjava/MaskedFlag.java b/src/main/java/org/lmdbjava/MaskedFlag.java index 9bdef63..242ca58 100644 --- a/src/main/java/org/lmdbjava/MaskedFlag.java +++ b/src/main/java/org/lmdbjava/MaskedFlag.java @@ -20,6 +20,11 @@ package org.lmdbjava; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Stream; + import static java.util.Objects.requireNonNull; /** @@ -34,25 +39,58 @@ public interface MaskedFlag { */ int getMask(); + /** + * Indicates if the flag must be propagated to the underlying C code of LMDB or not. + * + * @return the boolean value indicating the propagation + */ + default boolean isPropagatedToLmdb() { + return true; + } + + /** + * Fetch the integer mask for all presented flags. + * + * @param flags to mask (null or empty returns zero) + * @return the integer mask for use in C + */ + @SafeVarargs + static int mask(final M... flags) { + return mask(false, flags); + } + /** * Fetch the integer mask for all presented flags. * * @param flags to mask (null or empty returns zero) * @return the integer mask for use in C */ - static int mask(final MaskedFlag... flags) { - if (flags == null || flags.length == 0) { - return 0; - } + static int mask(final Stream flags) { + return mask(false, flags); + } + + /** + * Fetch the integer mask for the presented flags. + * + * @param onlyPropagatedToLmdb if to include only the flags which are also propagate to the C code or all of them + * @param flags to mask (null or empty returns zero) + * @return the integer mask for use in C + */ + @SafeVarargs + static int mask(final boolean onlyPropagatedToLmdb, final M... flags) { + return flags == null ? 0 : mask(onlyPropagatedToLmdb, Arrays.stream(flags)); + } + + /** + * Fetch the integer mask for all presented flags. + * + * @param onlyPropagatedToLmdb if to include only the flags which are also propagate to the C code or all of them + * @return the integer mask for use in C + */ + static int mask(final boolean onlyPropagatedToLmdb, final Stream flags) { + final Predicate filter = onlyPropagatedToLmdb ? MaskedFlag::isPropagatedToLmdb : f -> true; - int result = 0; - for (final MaskedFlag flag : flags) { - if (flag == null) { - continue; - } - result |= flag.getMask(); - } - return result; + return flags == null ? 0 : flags.filter(Objects::nonNull).filter(filter).map(M::getMask).reduce(0, (f1, f2) -> f1 | f2); } /** diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index b6d2891..3092c7c 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -20,6 +20,8 @@ package org.lmdbjava; +import jnr.ffi.Pointer; + import static jnr.ffi.Memory.allocateDirect; import static jnr.ffi.NativeType.ADDRESS; import static org.lmdbjava.Env.SHOULD_CHECK; @@ -34,8 +36,6 @@ import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; -import jnr.ffi.Pointer; - /** * LMDB transaction. * @@ -55,7 +55,7 @@ public final class Txn implements AutoCloseable { final TxnFlags... flags) { this.proxy = proxy; this.keyVal = proxy.keyVal(); - final int flagsMask = mask(flags); + final int flagsMask = mask(true, flags); this.readOnly = isSet(flagsMask, MDB_RDONLY_TXN); if (env.isReadOnly() && !this.readOnly) { throw new EnvIsReadOnly(); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy