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
8 changes: 8 additions & 0 deletions enumerator/usb_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package enumerator
import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"

Expand Down Expand Up @@ -110,5 +111,12 @@ func readLine(filename string) (string, error) {
defer file.Close()
reader := bufio.NewReader(file)
line, _, err := reader.ReadLine()
if err == io.EOF {
// An empty sysfs attribute file (e.g. an empty "serial",
// "manufacturer" or "product") yields io.EOF with no data.
// Treat it as an empty value, not an error, so a single device
// with a blank attribute does not abort the whole enumeration.
return "", nil
}
return string(line), err
}
103 changes: 103 additions & 0 deletions enumerator/usb_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//
// Copyright 2014-2026 Cristian Maglie. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//

package enumerator

import (
"os"
"path/filepath"
"testing"
)

// TestReadLineEmptyFile checks that readLine treats a zero-byte file (which is
// common for sysfs USB attributes such as "serial", "manufacturer" or
// "product" when the device exposes the attribute but leaves it blank) as an
// empty string, NOT as an error.
//
// Before the fix readLine returned io.EOF for an empty file. That error is
// propagated by parseUSBSysFS and aborts the whole GetDetailedPortsList()
// enumeration, so a single device with an empty (but present) sysfs attribute
// makes the entire port list unavailable.
func TestReadLineEmptyFile(t *testing.T) {
dir := t.TempDir()

empty := filepath.Join(dir, "empty")
if err := os.WriteFile(empty, []byte(""), 0o644); err != nil {
t.Fatal(err)
}
line, err := readLine(empty)
if err != nil {
t.Fatalf("readLine on an empty file returned error %v, want nil", err)
}
if line != "" {
t.Fatalf("readLine on an empty file returned %q, want empty string", line)
}
}

// TestReadLineMissingFile documents the already-working "attribute absent" case.
func TestReadLineMissingFile(t *testing.T) {
line, err := readLine(filepath.Join(t.TempDir(), "does-not-exist"))
if err != nil {
t.Fatalf("readLine on a missing file returned error %v, want nil", err)
}
if line != "" {
t.Fatalf("readLine on a missing file returned %q, want empty string", line)
}
}

// TestReadLineContent makes sure normal content (with and without a trailing
// newline) is still read correctly and the trailing newline is stripped.
func TestReadLineContent(t *testing.T) {
dir := t.TempDir()
cases := map[string]string{
"0403\n": "0403",
"6001": "6001",
}
for content, want := range cases {
f := filepath.Join(dir, "f")
if err := os.WriteFile(f, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
got, err := readLine(f)
if err != nil {
t.Fatalf("readLine(%q) error: %v", content, err)
}
if got != want {
t.Fatalf("readLine(%q) = %q, want %q", content, got, want)
}
}
}

// TestParseUSBSysFSEmptySerial reproduces the real-world failure: a USB device
// directory whose "serial" attribute exists but is empty. parseUSBSysFS must
// succeed and yield an empty SerialNumber, not fail enumeration.
func TestParseUSBSysFSEmptySerial(t *testing.T) {
dir := t.TempDir()
write := func(name, content string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("idVendor", "2341\n")
write("idProduct", "0043\n")
write("serial", "") // present but empty
write("manufacturer", "") // present but empty
write("product", "Arduino Uno\n")

details := &PortDetails{}
if err := parseUSBSysFS(dir, details); err != nil {
t.Fatalf("parseUSBSysFS returned error %v on a device with an empty serial attribute", err)
}
if details.VID != "2341" || details.PID != "0043" {
t.Fatalf("got VID=%q PID=%q, want 2341/0043", details.VID, details.PID)
}
if details.SerialNumber != "" {
t.Fatalf("SerialNumber = %q, want empty", details.SerialNumber)
}
if details.Product != "Arduino Uno" {
t.Fatalf("Product = %q, want %q", details.Product, "Arduino Uno")
}
}
Loading