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    *      http://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     public void setUp() {
143         codec = new BaseNCodec(0, 0, 0, 0) {
144             @Override
145             void decode(final byte[] pArray, final int i, final int length, final Context context) {
146             }
147 
148             @Override
149             void encode(final byte[] pArray, final int i, final int length, final Context context) {
150             }
151 
152             @Override
153             protected boolean isInAlphabet(final byte b) {
154                 return b=='O' || b == 'K'; // allow OK
155             }
156         };
157     }
158 
159     @Test
160     public void testBaseNCodec() {
161         assertNotNull(codec);
162     }
163 
164     @Test
165     public void testContainsAlphabetOrPad() {
166         assertFalse(codec.containsAlphabetOrPad(null));
167         assertFalse(codec.containsAlphabetOrPad(new byte[]{}));
168         assertTrue(codec.containsAlphabetOrPad("OK".getBytes()));
169         assertTrue(codec.containsAlphabetOrPad("OK ".getBytes()));
170         assertFalse(codec.containsAlphabetOrPad("ok ".getBytes()));
171         assertTrue(codec.containsAlphabetOrPad(new byte[]{codec.pad}));
172     }
173 
174     /**
175      * Test the Context string representation has debugging info.
176      * This is not part of the API and the test should be changed if the string
177      * format is updated.
178      */
179     @Test
180     public void testContextToString() {
181         final Context context = new Context();
182         context.buffer = new byte[3];
183         context.currentLinePos = 13;
184         context.eof = true;
185         context.ibitWorkArea = 777;
186         context.lbitWorkArea = 999;
187         context.modulus = 5;
188         context.pos = 42;
189         context.readPos = 981;
190         final String text = context.toString();
191         assertTrue(text.contains("[0, 0, 0]"));
192         assertTrue(text.contains("13"));
193         assertTrue(text.contains("true"));
194         assertTrue(text.contains("777"));
195         assertTrue(text.contains("999"));
196         assertTrue(text.contains("5"));
197         assertTrue(text.contains("42"));
198         assertTrue(text.contains("981"));
199     }
200 
201 //    @Test
202 //    public void testGetEncodedLength() {
203 //        fail("Not yet implemented");
204 //    }
205 
206     @Test
207     public void testEnsureBufferSize() {
208         final BaseNCodec ncodec = new NoOpBaseNCodec();
209         final Context context = new Context();
210         assertNull(context.buffer, "Initial buffer should be null");
211 
212         // Test initialization
213         context.pos = 76979;
214         context.readPos = 273;
215         ncodec.ensureBufferSize(0, context);
216         assertNotNull(context.buffer, "buffer should be initialized");
217         assertEquals(ncodec.getDefaultBufferSize(), context.buffer.length, "buffer should be initialized to default size");
218         assertEquals(0, context.pos, "context position");
219         assertEquals(0, context.readPos, "context read position");
220 
221         // Test when no expansion is required
222         ncodec.ensureBufferSize(1, context);
223         assertEquals(ncodec.getDefaultBufferSize(), context.buffer.length, "buffer should not expand unless required");
224 
225         // Test expansion
226         int length = context.buffer.length;
227         context.pos = length;
228         int extra = 1;
229         ncodec.ensureBufferSize(extra, context);
230         assertTrue(context.buffer.length >= length + extra, "buffer should expand");
231 
232         // Test expansion beyond double the buffer size.
233         // Hits the edge case where the required capacity is more than the default expansion.
234         length = context.buffer.length;
235         context.pos = length;
236         extra = length * 10;
237         ncodec.ensureBufferSize(extra, context);
238         assertTrue(context.buffer.length >= length + extra, "buffer should expand beyond double capacity");
239     }
240 
241     /**
242      * Test to expand to beyond the max buffer size.
243      *
244      * <p>Note: If the buffer is required to expand to above the max buffer size it may not work
245      * on all VMs and may have to be annotated with @Ignore.</p>
246      */
247     @Test
248     public void testEnsureBufferSizeExpandsToBeyondMaxBufferSize() {
249         assertEnsureBufferSizeExpandsToMaxBufferSize(true);
250     }
251 
252     /**
253      * Test to expand to the max buffer size.
254      */
255     @Test
256     public void testEnsureBufferSizeExpandsToMaxBufferSize() {
257         assertEnsureBufferSizeExpandsToMaxBufferSize(false);
258     }
259 
260     @Test
261     public void testEnsureBufferSizeThrowsOnOverflow() {
262         final BaseNCodec ncodec = new NoOpBaseNCodec();
263         final Context context = new Context();
264 
265         final int length = 10;
266         context.buffer = new byte[length];
267         context.pos = length;
268         final int extra = Integer.MAX_VALUE;
269         assertThrows(OutOfMemoryError.class, () -> ncodec.ensureBufferSize(extra, context));
270     }
271 
272     //
273 //    @Test
274 //    public void testEncodeObject() {
275 //        fail("Not yet implemented");
276 //    }
277 //
278 //    @Test
279 //    public void testEncodeToString() {
280 //        fail("Not yet implemented");
281 //    }
282 //
283 //    @Test
284 //    public void testDecodeObject() {
285 //        fail("Not yet implemented");
286 //    }
287 //
288 //    @Test
289 //    public void testDecodeString() {
290 //        fail("Not yet implemented");
291 //    }
292 //
293 //    @Test
294 //    public void testDecodeByteArray() {
295 //        fail("Not yet implemented");
296 //    }
297 //
298 //    @Test
299 //    public void testEncodeByteArray() {
300 //        fail("Not yet implemented");
301 //    }
302 //
303 //    @Test
304 //    public void testEncodeAsString() {
305 //        fail("Not yet implemented");
306 //    }
307 //
308 //    @Test
309 //    public void testEncodeByteArrayIntInt() {
310 //        fail("Not yet implemented");
311 //    }
312 //
313 //    @Test
314 //    public void testDecodeByteArrayIntInt() {
315 //        fail("Not yet implemented");
316 //    }
317 //
318     @Test
319     public void testIsInAlphabetByte() {
320         assertFalse(codec.isInAlphabet((byte) 0));
321         assertFalse(codec.isInAlphabet((byte) 'a'));
322         assertTrue(codec.isInAlphabet((byte) 'O'));
323         assertTrue(codec.isInAlphabet((byte) 'K'));
324     }
325 
326     @Test
327     public void testIsInAlphabetByteArrayBoolean() {
328         assertTrue(codec.isInAlphabet(new byte[] {}, false));
329         assertTrue(codec.isInAlphabet(new byte[] { 'O' }, false));
330         assertFalse(codec.isInAlphabet(new byte[] { 'O', ' ' }, false));
331         assertFalse(codec.isInAlphabet(new byte[] { ' ' }, false));
332         assertTrue(codec.isInAlphabet(new byte[] {}, true));
333         assertTrue(codec.isInAlphabet(new byte[] { 'O' }, true));
334         assertTrue(codec.isInAlphabet(new byte[] { 'O', ' ' }, true));
335         assertTrue(codec.isInAlphabet(new byte[] { ' ' }, true));
336     }
337 
338     @Test
339     public void testIsInAlphabetString() {
340         assertTrue(codec.isInAlphabet("OK"));
341         assertTrue(codec.isInAlphabet("O=K= \t\n\r"));
342     }
343 
344     //    @Test
345 //    public void testHasData() {
346 //        fail("Not yet implemented");
347 //    }
348 //
349 //    @Test
350 //    public void testAvail() {
351 //        fail("Not yet implemented");
352 //    }
353 //
354 //    @Test
355 //    public void testEnsureBufferSize() {
356 //        fail("Not yet implemented");
357 //    }
358 //
359 //    @Test
360 //    public void testReadResults() {
361 //        fail("Not yet implemented");
362 //    }
363 //
364     @Test
365     public void testIsWhiteSpace() {
366         assertTrue(Character.isWhitespace((byte) ' '));
367         assertTrue(Character.isWhitespace((byte) '\n'));
368         assertTrue(Character.isWhitespace((byte) '\r'));
369         assertTrue(Character.isWhitespace((byte) '\t'));
370         assertTrue(Character.isWhitespace((byte) '\f'));
371         assertTrue(Character.isWhitespace((byte) '\u000B'));
372         for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
373             final byte byteToCheck = b;
374             assertEquals(Character.isWhitespace(b), Character.isWhitespace(byteToCheck));
375         }
376     }
377 
378     @Test
379     public void testProvidePaddingByte() {
380         // Given
381         codec = new BaseNCodec(0, 0, 0, 0, (byte) 0x25) {
382             @Override
383             void decode(final byte[] pArray, final int i, final int length, final Context context) {
384                 // no-op
385             }
386 
387             @Override
388             void encode(final byte[] pArray, final int i, final int length, final Context context) {
389                 // no-op
390             }
391 
392             @Override
393             protected boolean isInAlphabet(final byte b) {
394                 return b == 'O' || b == 'K'; // allow OK
395             }
396         };
397 
398         // When
399         final byte actualPaddingByte = codec.pad;
400 
401         // Then
402         assertEquals(0x25, actualPaddingByte);
403     }
404 }