View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.binary;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNull;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  import static org.junit.jupiter.api.Assumptions.assumeTrue;
27  
28  import org.apache.commons.codec.binary.BaseNCodec.Context;
29  import org.junit.jupiter.api.BeforeEach;
30  import org.junit.jupiter.api.Test;
31  
32  public class BaseNCodecTest {
33  
34      /**
35       * Extend BaseNCodec without implementation (no operations = NoOp).
36       * Used for testing the memory allocation in {@link BaseNCodec#ensureBufferSize(int, Context)}.
37       */
38      private static final class NoOpBaseNCodec extends BaseNCodec {
39          NoOpBaseNCodec() {
40              super(0, 0, 0, 0);
41          }
42  
43          @Override
44          void decode(final byte[] pArray, final int i, final int length, final Context context) {
45              // no-op
46          }
47  
48          @Override
49          void encode(final byte[] pArray, final int i, final int length, final Context context) {
50              // no-op
51          }
52  
53          @Override
54          protected boolean isInAlphabet(final byte value) {
55              return false;
56          }
57      }
58  
59      private static void assertEnsureBufferSizeExpandsToMaxBufferSize(final boolean exceedMaxBufferSize) {
60          // This test is memory hungry.
61          // By default expansion will double the buffer size.
62          // Using a buffer that must be doubled to get close to 2GiB requires at least 3GiB
63          // of memory for the test (1GiB existing + 2GiB new).
64          // As a compromise we use an empty buffer and rely on the expansion switching
65          // to the minimum required capacity if doubling is not enough.
66  
67          // To effectively use a full buffer of ~1GiB change the following for: 1 << 30.
68          // Setting to zero has the lowest memory footprint for this test.
69          final int length = 0;
70  
71          final long presumableFreeMemory = getPresumableFreeMemory();
72          // 2GiB + 32 KiB + length
73          // 2GiB: Buffer to allocate
74          // 32KiB: Some headroom
75          // length: Existing buffer
76          final long estimatedMemory = (1L << 31) + 32 * 1024 + length;
77          assumeTrue(presumableFreeMemory > estimatedMemory, "Not enough free memory for the test");
78  
79          final int max = Integer.MAX_VALUE - 8;
80  
81          // Check the conservative maximum buffer size can actually be exceeded by the VM
82          // otherwise the test is not valid.
83          if (exceedMaxBufferSize) {
84              assumeCanAllocateBufferSize(max + 1);
85              // Free-memory.
86              // This may not be necessary as the byte[] is now out of scope
87              System.gc();
88          }
89  
90          final BaseNCodec ncodec = new NoOpBaseNCodec();
91          final Context context = new Context();
92  
93          // Allocate the initial buffer
94          context.buffer = new byte[length];
95          context.pos = length;
96          // Compute the extra to reach or exceed the max buffer size
97          int extra = max - length;
98          if (exceedMaxBufferSize) {
99              extra++;
100         }
101         ncodec.ensureBufferSize(extra, context);
102         assertTrue(context.buffer.length >= length + extra);
103     }
104 
105     /**
106      * Verify this VM can allocate the given size byte array. Otherwise skip the test.
107      */
108     private static void assumeCanAllocateBufferSize(final int size) {
109         byte[] bytes = null;
110         try {
111             bytes = new byte[size];
112         } catch (final OutOfMemoryError ignore) {
113             // ignore
114         }
115         assumeTrue(bytes != null, "Cannot allocate array of size: " + size);
116     }
117 
118     /**
119      * Gets the presumable free memory; an estimate of the amount of memory that could be allocated.
120      *
121      * <p>This performs a garbage clean-up and the obtains the presumed amount of free memory
122      * that can be allocated in this VM. This is computed as:<p>
123      *
124      * <pre>
125      * System.gc();
126      * long allocatedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
127      * long presumableFreeMemory = Runtime.getRuntime().maxMemory() - allocatedMemory;
128      * </pre>
129      *
130      * @return the presumable free memory
131      * @see <a href="https://stackoverflow.com/a/18366283">
132      *     Christian Fries StackOverflow answer on Java available memory</a>
133      */
134     static long getPresumableFreeMemory() {
135         System.gc();
136         final long allocatedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
137         return Runtime.getRuntime().maxMemory() - allocatedMemory;
138     }
139 
140     BaseNCodec codec;
141     @BeforeEach
142 
143     public void setUp() {
144         codec = new BaseNCodec(0, 0, 0, 0) {
145             @Override
146             void decode(final byte[] pArray, final int i, final int length, final Context context) {
147             }
148 
149             @Override
150             void encode(final byte[] pArray, final int i, final int length, final Context context) {
151             }
152 
153             @Override
154             protected boolean isInAlphabet(final byte b) {
155                 return b == 'O' || b == 'K'; // allow OK
156             }
157         };
158     }
159 
160     @Test
161     public void testBaseNCodec() {
162         assertNotNull(codec);
163     }
164 
165     @Test
166     public void testContainsAlphabetOrPad() {
167         assertFalse(codec.containsAlphabetOrPad(null));
168         assertFalse(codec.containsAlphabetOrPad(new byte[]{}));
169         assertTrue(codec.containsAlphabetOrPad("OK".getBytes()));
170         assertTrue(codec.containsAlphabetOrPad("OK ".getBytes()));
171         assertFalse(codec.containsAlphabetOrPad("ok ".getBytes()));
172         assertTrue(codec.containsAlphabetOrPad(new byte[]{codec.pad}));
173     }
174 
175     /**
176      * Test the Context string representation has debugging info.
177      * This is not part of the API and the test should be changed if the string
178      * format is updated.
179      */
180     @Test
181     public void testContextToString() {
182         final Context context = new Context();
183         context.buffer = new byte[3];
184         context.currentLinePos = 13;
185         context.eof = true;
186         context.ibitWorkArea = 777;
187         context.lbitWorkArea = 999;
188         context.modulus = 5;
189         context.pos = 42;
190         context.readPos = 981;
191         final String text = context.toString();
192         assertTrue(text.contains("[0, 0, 0]"));
193         assertTrue(text.contains("13"));
194         assertTrue(text.contains("true"));
195         assertTrue(text.contains("777"));
196         assertTrue(text.contains("999"));
197         assertTrue(text.contains("5"));
198         assertTrue(text.contains("42"));
199         assertTrue(text.contains("981"));
200     }
201 
202 //    @Test
203 //    public void testGetEncodedLength() {
204 //        fail("Not yet implemented");
205 //    }
206 
207     @Test
208     public void testEnsureBufferSize() {
209         final BaseNCodec ncodec = new NoOpBaseNCodec();
210         final Context context = new Context();
211         assertNull(context.buffer, "Initial buffer should be null");
212 
213         // Test initialization
214         context.pos = 76979;
215         context.readPos = 273;
216         ncodec.ensureBufferSize(0, context);
217         assertNotNull(context.buffer, "buffer should be initialized");
218         assertEquals(ncodec.getDefaultBufferSize(), context.buffer.length, "buffer should be initialized to default size");
219         assertEquals(0, context.pos, "context position");
220         assertEquals(0, context.readPos, "context read position");
221 
222         // Test when no expansion is required
223         ncodec.ensureBufferSize(1, context);
224         assertEquals(ncodec.getDefaultBufferSize(), context.buffer.length, "buffer should not expand unless required");
225 
226         // Test expansion
227         int length = context.buffer.length;
228         context.pos = length;
229         int extra = 1;
230         ncodec.ensureBufferSize(extra, context);
231         assertTrue(context.buffer.length >= length + extra, "buffer should expand");
232 
233         // Test expansion beyond double the buffer size.
234         // Hits the edge case where the required capacity is more than the default expansion.
235         length = context.buffer.length;
236         context.pos = length;
237         extra = length * 10;
238         ncodec.ensureBufferSize(extra, context);
239         assertTrue(context.buffer.length >= length + extra, "buffer should expand beyond double capacity");
240     }
241 
242     /**
243      * Test to expand to beyond the max buffer size.
244      *
245      * <p>Note: If the buffer is required to expand to above the max buffer size it may not work
246      * on all VMs and may have to be annotated with @Ignore.</p>
247      */
248     @Test
249     public void testEnsureBufferSizeExpandsToBeyondMaxBufferSize() {
250         assertEnsureBufferSizeExpandsToMaxBufferSize(true);
251     }
252 
253     /**
254      * Test to expand to the max buffer size.
255      */
256     @Test
257     public void testEnsureBufferSizeExpandsToMaxBufferSize() {
258         assertEnsureBufferSizeExpandsToMaxBufferSize(false);
259     }
260 
261     @Test
262     public void testEnsureBufferSizeThrowsOnOverflow() {
263         final BaseNCodec ncodec = new NoOpBaseNCodec();
264         final Context context = new Context();
265 
266         final int length = 10;
267         context.buffer = new byte[length];
268         context.pos = length;
269         final int extra = Integer.MAX_VALUE;
270         assertThrows(OutOfMemoryError.class, () -> ncodec.ensureBufferSize(extra, context));
271     }
272 
273     //
274 //    @Test
275 //    public void testEncodeObject() {
276 //        fail("Not yet implemented");
277 //    }
278 //
279 //    @Test
280 //    public void testEncodeToString() {
281 //        fail("Not yet implemented");
282 //    }
283 //
284 //    @Test
285 //    public void testDecodeObject() {
286 //        fail("Not yet implemented");
287 //    }
288 //
289 //    @Test
290 //    public void testDecodeString() {
291 //        fail("Not yet implemented");
292 //    }
293 //
294 //    @Test
295 //    public void testDecodeByteArray() {
296 //        fail("Not yet implemented");
297 //    }
298 //
299 //    @Test
300 //    public void testEncodeByteArray() {
301 //        fail("Not yet implemented");
302 //    }
303 //
304 //    @Test
305 //    public void testEncodeAsString() {
306 //        fail("Not yet implemented");
307 //    }
308 //
309 //    @Test
310 //    public void testEncodeByteArrayIntInt() {
311 //        fail("Not yet implemented");
312 //    }
313 //
314 //    @Test
315 //    public void testDecodeByteArrayIntInt() {
316 //        fail("Not yet implemented");
317 //    }
318 //
319     @Test
320     public void testIsInAlphabetByte() {
321         assertFalse(codec.isInAlphabet((byte) 0));
322         assertFalse(codec.isInAlphabet((byte) 'a'));
323         assertTrue(codec.isInAlphabet((byte) 'O'));
324         assertTrue(codec.isInAlphabet((byte) 'K'));
325     }
326 
327     @Test
328     public void testIsInAlphabetByteArrayBoolean() {
329         assertTrue(codec.isInAlphabet(new byte[] {}, false));
330         assertTrue(codec.isInAlphabet(new byte[] { 'O' }, false));
331         assertFalse(codec.isInAlphabet(new byte[] { 'O', ' ' }, false));
332         assertFalse(codec.isInAlphabet(new byte[] { ' ' }, false));
333         assertTrue(codec.isInAlphabet(new byte[] {}, true));
334         assertTrue(codec.isInAlphabet(new byte[] { 'O' }, true));
335         assertTrue(codec.isInAlphabet(new byte[] { 'O', ' ' }, true));
336         assertTrue(codec.isInAlphabet(new byte[] { ' ' }, true));
337     }
338 
339     @Test
340     public void testIsInAlphabetString() {
341         assertTrue(codec.isInAlphabet("OK"));
342         assertTrue(codec.isInAlphabet("O=K= \t\n\r"));
343     }
344 
345     //    @Test
346 //    public void testHasData() {
347 //        fail("Not yet implemented");
348 //    }
349 //
350 //    @Test
351 //    public void testAvail() {
352 //        fail("Not yet implemented");
353 //    }
354 //
355 //    @Test
356 //    public void testEnsureBufferSize() {
357 //        fail("Not yet implemented");
358 //    }
359 //
360 //    @Test
361 //    public void testReadResults() {
362 //        fail("Not yet implemented");
363 //    }
364 //
365     @Test
366     public void testIsWhiteSpace() {
367         assertTrue(Character.isWhitespace((byte) ' '));
368         assertTrue(Character.isWhitespace((byte) '\n'));
369         assertTrue(Character.isWhitespace((byte) '\r'));
370         assertTrue(Character.isWhitespace((byte) '\t'));
371         assertTrue(Character.isWhitespace((byte) '\f'));
372         assertTrue(Character.isWhitespace((byte) '\u000B'));
373         for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
374             final byte byteToCheck = b;
375             assertEquals(Character.isWhitespace(b), Character.isWhitespace(byteToCheck));
376         }
377     }
378 
379     @Test
380     public void testProvidePaddingByte() {
381         // Given
382         codec = new BaseNCodec(0, 0, 0, 0, (byte) 0x25) {
383             @Override
384             void decode(final byte[] pArray, final int i, final int length, final Context context) {
385                 // no-op
386             }
387 
388             @Override
389             void encode(final byte[] pArray, final int i, final int length, final Context context) {
390                 // no-op
391             }
392 
393             @Override
394             protected boolean isInAlphabet(final byte b) {
395                 return b == 'O' || b == 'K'; // allow OK
396             }
397         };
398 
399         // When
400         final byte actualPaddingByte = codec.pad;
401 
402         // Then
403         assertEquals(0x25, actualPaddingByte);
404     }
405 }