Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/main/com/portalmedia/embarc/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.portalmedia.embarc.gui.model.DPXFileInformationViewModel;
import com.portalmedia.embarc.gui.mxf.MXFProfileULMap;
import com.portalmedia.embarc.parser.ColumnDef;
import com.portalmedia.embarc.parser.BytesToStringHelper;
import com.portalmedia.embarc.parser.FileFormat;
import com.portalmedia.embarc.parser.FileFormatDetection;
import com.portalmedia.embarc.parser.FileInformation;
Expand Down Expand Up @@ -103,6 +104,7 @@ public static void main(String[] args) throws IOException {
Option downloadTDStream = Option.builder("downloadTDStream").desc("MXF: Select a text stream to download").hasArg().build();
Option downloadBDStream = Option.builder("downloadBDStream").desc("MXF: Select a binary stream to download").hasArg().build();
Option downloadStreamOutputPath = Option.builder("streamOutputPath").desc("MXF: Output directory for selected stream").hasArg().build();
Option mixedAsciiMode = Option.builder("mixedAsciiMode").desc("Toggle mixed ASCII/non-ASCII display mode (0=preserve ASCII, 1=all hex)").hasArg().build();

options.addOption(printHelpOption);
options.addOption(printMetadataOption);
Expand All @@ -114,6 +116,7 @@ public static void main(String[] args) throws IOException {
options.addOption(downloadTDStream);
options.addOption(downloadBDStream);
options.addOption(downloadStreamOutputPath);
options.addOption(mixedAsciiMode);

formatter = new HelpFormatter();
formatter.setOptionComparator(null);
Expand All @@ -127,6 +130,23 @@ public static void main(String[] args) throws IOException {
formatter.printHelp("embARC CLI", options);
return;
}

// Handle mixed ASCII display mode option
if (line.hasOption("mixedAsciiMode")) {
String modeStr = line.getOptionValue("mixedAsciiMode");
try {
int mode = Integer.parseInt(modeStr);
if (mode == BytesToStringHelper.MODE_PRESERVE_ASCII || mode == BytesToStringHelper.MODE_ALL_HEX) {
BytesToStringHelper.setMixedAsciiDisplayMode(mode);
System.out.println("\nMixed ASCII display mode set to: " +
(mode == BytesToStringHelper.MODE_PRESERVE_ASCII ? "preserve ASCII (0)" : "all hex (1)"));
} else {
System.out.println("\nInvalid mode value. Use 0 for preserve ASCII or 1 for all hex.");
}
} catch (NumberFormatException e) {
System.out.println("\nInvalid mode value. Use 0 for preserve ASCII or 1 for all hex.");
}
}

if (otherArgs.size() > 1) {
System.out.println("\nToo many arguments\n");
Expand Down
80 changes: 77 additions & 3 deletions src/main/com/portalmedia/embarc/parser/BytesToStringHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.prefs.Preferences;

/**
* Helper method to turn bytes arrays into types and then into strings
Expand All @@ -14,6 +16,26 @@
* @since 2019-05-01
**/
public class BytesToStringHelper {
// Preference key for mixed ASCII display mode
private static final String PREF_MIXED_MODE = "mixedAsciiDisplayMode";
// Display modes
public static final int MODE_PRESERVE_ASCII = 0; // Show ASCII portions as text, non-ASCII as hex
public static final int MODE_ALL_HEX = 1; // Show entire field as hex if any non-ASCII present

// Get user preference for mixed ASCII display mode
public static int getMixedAsciiDisplayMode() {
Preferences prefs = Preferences.userNodeForPackage(BytesToStringHelper.class);
return prefs.getInt(PREF_MIXED_MODE, MODE_PRESERVE_ASCII); // Default to preserving ASCII
}

// Set user preference for mixed ASCII display mode
public static void setMixedAsciiDisplayMode(int mode) {
if (mode != MODE_PRESERVE_ASCII && mode != MODE_ALL_HEX) {
throw new IllegalArgumentException("Invalid display mode");
}
Preferences prefs = Preferences.userNodeForPackage(BytesToStringHelper.class);
prefs.putInt(PREF_MIXED_MODE, mode);
}
private static String[] getStringIntArray(byte[] value, ByteOrder byteOrder) {
final String[] toReturn = new String[value.length / 4];
int returnIndex = 0;
Expand Down Expand Up @@ -47,7 +69,7 @@ private static Integer[] getShortArray(byte[] value, ByteOrder byteOrder) {
final Integer[] toReturn = new Integer[value.length / 2];
int returnIndex = 0;
for (int i = 0; i < value.length; i = i + 2) {
toReturn[returnIndex] = ByteBuffer.wrap(value).order(byteOrder).getShort() & 0xffff1;
toReturn[returnIndex] = ByteBuffer.wrap(value).order(byteOrder).getShort() & 0xffff;
returnIndex++;
}
return toReturn;
Expand Down Expand Up @@ -126,10 +148,62 @@ public static int getInteger(byte[] value, ByteOrder byteOrder) {
}

public static String toString(byte[] value) {
return new String(value).replaceAll("\\u0000", "");
// Check if array is empty
if (value == null || value.length == 0) {
return "";
}

// First scan to determine if we have any non-ASCII characters
boolean hasNonAscii = false;
for (byte b : value) {
// Skip null bytes when checking
if (b == 0x00) continue;

// Check if outside ASCII printable range (32-126)
if (b < 32 || b > 126) {
hasNonAscii = true;
break;
}
}

// If pure ASCII, just return as string
if (!hasNonAscii) {
StringBuilder result = new StringBuilder();
for (byte b : value) {
if (b == 0x00) continue; // Skip null bytes
result.append((char)b);
}
return result.toString();
}

// Get display mode preference
int displayMode = getMixedAsciiDisplayMode();

// If it contains non-ASCII and mode is ALL_HEX, return hex representation
if (displayMode == MODE_ALL_HEX) {
return getHexString(value, ByteOrder.BIG_ENDIAN);
}

// Otherwise preserve ASCII portions (MODE_PRESERVE_ASCII)
StringBuilder result = new StringBuilder();
for (byte b : value) {
// Skip null bytes
if (b == 0x00) continue;

// ASCII printable range (32-126)
if (b >= 32 && b <= 126) {
result.append((char)b);
} else {
result.append(String.format("[0x%02x]", b));
}
}

return result.toString();
}

public static String toTypedString(Class<?> type, byte[] value, ByteOrder byteOrder) {
if(isNull(value, type)) return "NULL";

if (type == byte[].class) {
return toByteArrayString(value);
} else if (type == byte.class) {
Expand All @@ -154,7 +228,7 @@ public static String toTypedString(Class<?> type, byte[] value, ByteOrder byteOr
}

private static Boolean isValidString(byte[] value, ByteOrder byteOrder) {
CharsetDecoder d = Charset.forName("US-ASCII").newDecoder();
CharsetDecoder d = Charset.forName("UTF-8").newDecoder();
try {
CharBuffer r = d.decode(ByteBuffer.wrap(value).order(byteOrder));
r.toString();
Expand Down
89 changes: 89 additions & 0 deletions src/test/com/portalmedia/embarc/cli/MixedAsciiDisplayTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.portalmedia.embarc.cli;

import static org.junit.jupiter.api.Assertions.*;

import java.nio.ByteOrder;
import org.junit.jupiter.api.Test;

/**
* Tests for mixed ASCII/non-ASCII display behavior (Issue #50)
*
* VALIDATION REQUIREMENTS:
* 1. Pure ASCII text must display unchanged
* 2. Non-ASCII chars should show as {0xXXXX} hex codes
* 3. Mixed content should preserve ASCII portions
* 4. Empty/null input should handle gracefully
*/
public class MixedAsciiDisplayTest {

@Test
public void testMixedAsciiNonAsciiDisplay() {
// Pure ASCII
byte[] asciiBytes = {'H', 'e', 'l', 'l', 'o'};
String asciiResult = BytesToStringHelper.toString(asciiBytes);
assertEquals("Hello", asciiResult);

// Pure non-ASCII (should show as HEX)
byte[] nonAsciiBytes = {0x00, (byte)0xA9, (byte)0xAE};
String nonAsciiResult = BytesToStringHelper.toString(nonAsciiBytes);
assertTrue(nonAsciiResult.startsWith("HEX: "));

// Mixed ASCII/non-ASCII (current behavior shows as HEX)
byte[] mixedBytes = {
'A', 'B', 'C', // ASCII
0x00, (byte)0xA9, (byte)0xAE, // Non-ASCII
'D', 'E', 'F' // ASCII
};
String mixedResult = BytesToStringHelper.toString(mixedBytes);
System.out.println("Mixed result: " + mixedResult);
assertTrue(mixedResult.startsWith("HEX: "),
"Current behavior shows mixed content as HEX");
}

@Test
public void testPureAsciiDisplay() {
String pureAscii = "This is pure ASCII";
String displayed = BytesToStringHelper.toString(pureAscii.getBytes());
assertEquals(pureAscii, displayed,
"Pure ASCII should remain unchanged");
}

@Test
public void testPureNonAsciiDisplay() {
String pureNonAscii = "日本語";
String displayed = BytesToStringHelper.toString(pureNonAscii.getBytes());
assertTrue(displayed.matches("^\\{0x[0-9a-f]+\\}.*"),
"Pure non-ASCII should be encoded");
assertFalse(displayed.contains("日本語"),
"Non-ASCII characters should not appear directly");
}

@Test
public void testMixedAsciiNonAsciiDisplayIssue() {
// Test with mixed ASCII and non-ASCII characters
byte[] mixedBytes = {
'A', 'B', 'C', // ASCII
0x00, (byte)0xA9, (byte)0xAE, // Non-ASCII
'D', 'E', 'F' // ASCII
};

// Test with ASCII only
byte[] asciiBytes = {'H', 'e', 'l', 'l', 'o'};

// Test with non-ASCII only
byte[] nonAsciiBytes = {0x00, (byte)0xA9, (byte)0xAE};

String mixedResult = BytesToStringHelper.toString(mixedBytes);
String asciiResult = BytesToStringHelper.toString(asciiBytes);
String nonAsciiResult = BytesToStringHelper.toString(nonAsciiBytes);

System.out.println("Mixed result: " + mixedResult);
System.out.println("ASCII result: " + asciiResult);
System.out.println("Non-ASCII result: " + nonAsciiResult);

// Current behavior - all non-ASCII shows as hex
assertTrue(mixedResult.contains("HEX"));
assertFalse(asciiResult.contains("HEX"));
assertTrue(nonAsciiResult.contains("HEX"));
}
}
Binary file added src/test/standalone/BytesToStringVerifier.class
Binary file not shown.
35 changes: 35 additions & 0 deletions src/test/standalone/BytesToStringVerifier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package standalone;

public class BytesToStringVerifier {

public static String toString(byte[] value) {
StringBuilder result = new StringBuilder();
boolean isAscii = true;

for (byte b : value) {
// Skip null bytes
if (b == 0x00) continue;

// ASCII printable range (32-126)
if (b >= 32 && b <= 126) {
result.append((char)b);
} else {
result.append(String.format("[0x%02x]", b));
isAscii = false;
}
}

return result.toString();
}

public static void main(String[] args) {
// Test cases
byte[] pureAscii = {'H', 'e', 'l', 'l', 'o'};
byte[] pureNonAscii = {0x00, (byte)0xA9, (byte)0xAE};
byte[] mixed = {'A', 'B', 'C', 0x00, (byte)0xA9, 'D', 'E', 'F'};

System.out.println("Pure ASCII: " + toString(pureAscii));
System.out.println("Pure Non-ASCII: " + toString(pureNonAscii));
System.out.println("Mixed Content: " + toString(mixed));
}
}
25 changes: 25 additions & 0 deletions src/test/standalone/MixedAsciiDemo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package standalone;

import com.portalmedia.embarc.parser.BytesToStringHelper;

public class MixedAsciiDemo {
public static void main(String[] args) {
// Test with mixed ASCII and non-ASCII characters
byte[] mixedBytes = {
'A', 'B', 'C', // ASCII
0x00, (byte)0xA9, (byte)0xAE, // Non-ASCII
'D', 'E', 'F' // ASCII
};

String result = BytesToStringHelper.toString(mixedBytes);
System.out.println("Mixed ASCII/non-ASCII output: " + result);

// Test with ASCII only
byte[] asciiBytes = {'H', 'e', 'l', 'l', 'o'};
System.out.println("ASCII only output: " + BytesToStringHelper.toString(asciiBytes));

// Test with non-ASCII only
byte[] nonAsciiBytes = {0x00, (byte)0xA9, (byte)0xAE};
System.out.println("Non-ASCII only output: " + BytesToStringHelper.toString(nonAsciiBytes));
}
}
24 changes: 24 additions & 0 deletions src/test/standalone/MixedAsciiTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package standalone;

import com.portalmedia.embarc.parser.BytesToStringHelper;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class MixedAsciiTest {

@Test
public void testMixedAsciiNonAsciiDisplay() {
// Test with mixed ASCII and non-ASCII characters
byte[] mixedBytes = {
'A', 'B', 'C', // ASCII
0x00, (byte)0xA9, (byte)0xAE, // Non-ASCII
'D', 'E', 'F' // ASCII
};

String result = BytesToStringHelper.toString(mixedBytes);
System.out.println("Mixed result: " + result);

// Current behavior - all non-ASCII shows as hex
assertTrue(result.contains("HEX"));
}
}