Skip to content

Commit

Permalink
Windows: Handle invalid string descriptors
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelbl committed Dec 6, 2024
1 parent 2de2bb0 commit 8497de1
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

package net.codecrete.usb.usbstandard;

import net.codecrete.usb.UsbException;

import java.lang.foreign.GroupLayout;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.nio.charset.StandardCharsets;

import static java.lang.foreign.ValueLayout.JAVA_BYTE;
import static java.lang.foreign.ValueLayout.JAVA_CHAR;
import static java.lang.foreign.ValueLayout.JAVA_SHORT;

/**
Expand All @@ -27,13 +29,46 @@ public StringDescriptor(MemorySegment descriptor) {
this.descriptor = descriptor;
}

/**
* Indicates if this string descriptor is valid.
* <p>
* Invalid string descriptors might be missing the header,
* have a descriptor type that is not a string descriptor,
* indicate an incorrect length or have incomplete UTF-16 code units.
* </p>
* @return if this descriptor is valid
*/
public boolean isValid() {
return descriptor.byteSize() >= 2
&& descriptor.get(JAVA_BYTE, bDescriptorType$OFFSET) == 3
&& length() == descriptor.byteSize()
&& (descriptor.byteSize() & 1) == 0;
}

public int length() {
return 0xff & descriptor.get(JAVA_BYTE, bLength$OFFSET);
}

/**
* Returns the string of this string descriptor.
* <p>
* Invalid UTF-16 code units are replaced with the Unicode replacement character.
* Trailing 0s (UTF-16 code unit with value 0) are truncated.
* </p>
* @throws UsbException if the string descriptor is invalid
* @return the string value
*/
public String string() {
var chars = descriptor.asSlice(string$OFFSET, length() - 2L).toArray(JAVA_CHAR);
return new String(chars);
if (!isValid())
throw new UsbException("String descriptor is invalid");
var len = (int)(length() - 2L);
var bytes = descriptor.asSlice(string$OFFSET, len).toArray(JAVA_BYTE);

// truncate trailing 0s
while (len > 0 && bytes[len-2] == 0 && bytes[len-1] == 0)
len--;

return new String(bytes, 0, len, StandardCharsets.UTF_16LE);
}

// struct USBStringDescriptor {
Expand All @@ -48,5 +83,6 @@ public String string() {
);

private static final long bLength$OFFSET = 0;
private static final long bDescriptorType$OFFSET = 1;
private static final long string$OFFSET = 2;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package net.codecrete.usb;

import net.codecrete.usb.usbstandard.StringDescriptor;
import org.junit.jupiter.api.Test;

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class StringDescriptorTest {

@Test
void validStringDescriptor() {
testDescriptor(
new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0},
stringDescriptor -> {
assertThat(stringDescriptor.isValid()).isTrue();
assertThat(stringDescriptor.string()).isEqualTo("Hello");
}
);
}

@Test
void truncateTrailingZeros() {
testDescriptor(
new byte[]{0x0e, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
stringDescriptor -> {
assertThat(stringDescriptor.isValid()).isTrue();
assertThat(stringDescriptor.string()).isEqualTo("Hello");
}
);
}

@Test
void invalidDescriptorType() {
testDescriptor(
new byte[]{0x0c, 0x04, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0},
stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
);
}

@Test
void inconsistentLength() {
testDescriptor(
new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
);
}

@Test
void inconsistentLengthThrowsException() {
testDescriptor(
new byte[]{0x0c, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0, 0},
stringDescriptor -> {
assertThat(stringDescriptor.isValid()).isFalse();
assertThatThrownBy(stringDescriptor::string)
.isInstanceOf(UsbException.class)
.hasMessage("String descriptor is invalid");
}
);
}

@Test
void oddLength() {
testDescriptor(
new byte[]{0x0d, 0x03, 'H', 0, 'e', 0, 'l', 0, 'l', 0, 'o', 0, 0},
stringDescriptor -> assertThat(stringDescriptor.isValid()).isFalse()
);
}

@Test
void unicodeSurrogates() {
// In theory, USB is stuck with an old Unicode standard and the below is invalid.
// In practice, it will work anyway.
testDescriptor(
new byte[]{0x06, 0x03, 0x3D, (byte)0xD8, 0x1B, (byte)0xDE},
stringDescriptor -> {
assertThat(stringDescriptor.isValid()).isTrue();
assertThat(stringDescriptor.string()).isEqualTo("\uD83D\uDE1B");
}
);
}

@Test
void invalidUnicodeCharactersAreReplaced() {
testDescriptor(
new byte[]{0x06, 0x03, 'H', 0, 0x1B, (byte)0xDE},
stringDescriptor -> {
assertThat(stringDescriptor.isValid()).isTrue();
assertThat(stringDescriptor.string()).isEqualTo("H�");
}
);
}

private void testDescriptor(byte[] descriptorBytes, Consumer<StringDescriptor> validator) {
try (var arena = Arena.ofConfined()) {
var memorySegment = arena.allocate(descriptorBytes.length);
memorySegment.copyFrom(MemorySegment.ofArray(descriptorBytes));

var stringDescriptor = new StringDescriptor(memorySegment);
validator.accept(stringDescriptor);
}
}
}

0 comments on commit 8497de1

Please sign in to comment.