Skip to content
Open
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
41 changes: 25 additions & 16 deletions internal/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ import (
// using the reliable package.
func ParseInto[P types.ParameterView](ctx *context.Context[P], dest *types.Command[P]) error {
err := readCommandHeaderInto(ctx.Reader, dest)
if err != nil {
return err
}

if dest.Type > types.SendReliableFragmentCommand {
if !types.IsKnownCommandType(dest.Type) {
remaining := ctx.Reader.Max - ctx.Reader.Cursor - 1

if ctx.Config.SkipUnknownPayloads {
Expand All @@ -45,30 +48,33 @@ func ParseInto[P types.ParameterView](ctx *context.Context[P], dest *types.Comma
return nil
}

if err != nil {
return err
}

if dest.Length < types.COMMAND_HEADER_SIZE {
return errors.ErrHeaderSize
}

payloadStart := ctx.Reader.Cursor
payloadLen := int(dest.Length - types.COMMAND_HEADER_SIZE)
commandEnd := payloadStart + payloadLen

if ctx.Config.SkipCommands[dest.Type] {
remaining := int(dest.Length - types.COMMAND_HEADER_SIZE)
return ctx.Reader.Skip(remaining)
return ctx.Reader.Skip(payloadLen)
}

err = readCommandPayloadInto(ctx, dest)
if err != nil {
remaining := int(dest.Length - types.COMMAND_HEADER_SIZE)
remaining := commandEnd - ctx.Reader.Cursor
if remaining < 0 || commandEnd > ctx.Reader.Max {
return err
}

if ctx.Config.SkipUnknownPayloads {
return ctx.Reader.Skip(remaining)
}

rest, _ := ctx.Reader.ReadBytes(remaining)
// don't fatal — just store raw for encrypted packets
dest.UnknownPayload = types.UnknownPayload{Raw: rest, Kind: dest.Type}
// Don't fatal — store the full raw command payload for encrypted or
// otherwise unsupported payloads, then advance to the next command.
dest.UnknownPayload = types.UnknownPayload{Raw: ctx.Reader.Buffer[payloadStart:commandEnd], Kind: dest.Type}
ctx.Reader.Cursor = commandEnd
}

emit(ctx.Hooks, dest)
Expand All @@ -86,7 +92,7 @@ func readCommandHeaderInto[P types.ParameterView](r *reader.Reader, dest *types.

dest.Type = types.CommandType(b)

if dest.Type > types.SendReliableFragmentCommand {
if !types.IsKnownCommandType(dest.Type) {
return nil
}

Expand Down Expand Up @@ -120,7 +126,7 @@ func readCommandHeaderInto[P types.ParameterView](r *reader.Reader, dest *types.

func readCommandPayloadInto[P types.ParameterView](ctx *context.Context[P], dest *types.Command[P]) error {
switch dest.Type {
case types.SendUnreliableCommand:
case types.SendUnreliableCommand, types.SendUnreliableUnsequenced:

_, err := ctx.Reader.ReadBytes(4)
if err != nil {
Expand Down Expand Up @@ -162,6 +168,8 @@ func readCommandPayloadInto[P types.ParameterView](ctx *context.Context[P], dest
}
case types.PingCommand:
dest.PingPayload = struct{}{}
case types.FetchServerTimestampCommand:
dest.FetchTimestampPayload = struct{}{}
case types.DisconnectCommand:
dest.DisconnectPayload = struct{}{}
default:
Expand All @@ -180,14 +188,15 @@ func emit[P types.ParameterView](hooks *hooks.Hooks[P], dest *types.Command[P])
hooks.SyncHooks.OnCommand(*dest)
}

if hooks.AsyncHooks.OnCommand == nil {
ch := hooks.AsyncHooks.OnCommand
if ch == nil || len(ch) == cap(ch) {
return
}

s := DetachForAsync(*dest)

select {
case hooks.AsyncHooks.OnCommand <- s:
case ch <- s:
default: // don't block parser
}

Expand All @@ -202,7 +211,7 @@ func DetachForAsync[P types.ParameterView](cmd types.Command[P]) types.Command[P
copy(p, s.ReliablePayload.Parameters)
s.ReliablePayload.Parameters = p
}
case types.SendUnreliableCommand:
case types.SendUnreliableCommand, types.SendUnreliableUnsequenced:
if n := len(s.UnreliablePayload.Parameters); n > 0 {
p := make([]P, n)
copy(p, s.UnreliablePayload.Parameters)
Expand Down
30 changes: 30 additions & 0 deletions internal/command/command_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package command

import (
"testing"

"github.com/AutoDruid/photon-parser/internal/hooks"
v18 "github.com/AutoDruid/photon-parser/internal/parameters/v18"
"github.com/AutoDruid/photon-parser/internal/types"
)

func TestAsyncCommandEmitFullChannelDoesNotAllocate(t *testing.T) {
h := hooks.NewHooks[v18.Parameter]()
h.OnCommandAsync(types.HookOptions{Size: 1})
h.AsyncHooks.OnCommand <- types.Command[v18.Parameter]{}

cmd := types.Command[v18.Parameter]{
CommandHeader: types.CommandHeader{Type: types.SendReliableCommand},
ReliablePayload: types.Reliable[v18.Parameter]{
Parameters: []v18.Parameter{{Header: v18.Header{ID: 1, Type: v18.Int8Type}}},
},
}

allocs := testing.AllocsPerRun(100, func() {
emit(h, &cmd)
})

if allocs != 0 {
t.Fatalf("emit() allocations on full async channel = %v, want 0", allocs)
}
}
92 changes: 92 additions & 0 deletions internal/command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,95 @@ func TestParseSession(t *testing.T) {
t.Fatalf("LoadFromWiresharkExport() failed: %v", err)
}
}

func TestParseSendUnreliableUnsequencedCommand(t *testing.T) {
payload := []byte{
0x0b, 0x01, 0x02, 0x04, 0x00, 0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3e,
0xf3, 0x02, 0x02, 0x02, 0x00, 0x0b, 0x79, 0x01, 0x05, 0x00, 0x00, 0x00, 0x00,
}

ctx := &context.Context[v18.Parameter]{
Reader: reader.NewReader(payload),
Decoders: context.Decoders[v18.Parameter]{
ParameterParser: &v18.Parameter{},
ReliableHeaderParameterCount: &v18.ReliableHeaderParameterCountV18{},
},
PoolParameter: context.NewPool[v18.Parameter](10),
PoolCommand: context.NewPool[types.Command[v18.Parameter]](10),
}

var cmd types.Command[v18.Parameter]
if err := command.ParseInto(ctx, &cmd); err != nil {
t.Fatalf("parse send unreliable unsequenced: %v", err)
}

if cmd.Type != 0x0b {
t.Fatalf("command type: got %d, want 11", cmd.Type)
}

if got := len(cmd.UnknownPayload.Raw); got != 0 {
t.Fatalf("unknown payload length: got %d, want 0", got)
}

if cmd.UnreliablePayload.Type != types.OperationRequest {
t.Fatalf("unreliable payload type: got %d, want %d", cmd.UnreliablePayload.Type, types.OperationRequest)
}

if cmd.UnreliablePayload.EventCode != 0x02 {
t.Fatalf("event code: got %d, want 2", cmd.UnreliablePayload.EventCode)
}

if cmd.UnreliablePayload.ParameterCount != 2 {
t.Fatalf("parameter count: got %d, want 2", cmd.UnreliablePayload.ParameterCount)
}

if got := len(cmd.UnreliablePayload.Parameters); got != 2 {
t.Fatalf("parameters length: got %d, want 2", got)
}
if cmd.UnreliablePayload.Parameters[0].Header.ID != 0x00 {
t.Fatalf("first parameter id: got %d, want 0", cmd.UnreliablePayload.Parameters[0].Header.ID)
}
if cmd.UnreliablePayload.Parameters[1].Header.ID != 0x01 {
t.Fatalf("second parameter id: got %d, want 1", cmd.UnreliablePayload.Parameters[1].Header.ID)
}
}

func TestParseFetchServerTimestampCommand(t *testing.T) {
payload := []byte{0x0c, 0xff, 0x01, 0x04, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x02}

ctx := &context.Context[v18.Parameter]{
Reader: reader.NewReader(payload),
Decoders: context.Decoders[v18.Parameter]{
ParameterParser: &v18.Parameter{},
ReliableHeaderParameterCount: &v18.ReliableHeaderParameterCountV18{},
},
PoolParameter: context.NewPool[v18.Parameter](10),
PoolCommand: context.NewPool[types.Command[v18.Parameter]](10),
}

var cmd types.Command[v18.Parameter]
if err := command.ParseInto(ctx, &cmd); err != nil {
t.Fatalf("parse fetch server timestamp: %v", err)
}

if cmd.Type != 0x0c {
t.Fatalf("command type: got %d, want 12", cmd.Type)
}

if got := len(cmd.UnknownPayload.Raw); got != 0 {
t.Fatalf("unknown payload length: got %d, want 0", got)
}

if cmd.ChannelID != 0xff {
t.Fatalf("channel id: got %d, want 255", cmd.ChannelID)
}

if cmd.Length != 12 {
t.Fatalf("length: got %d, want 12", cmd.Length)
}

if cmd.ReliableSequenceNumber != 2 {
t.Fatalf("reliable sequence number: got %d, want 2", cmd.ReliableSequenceNumber)
}
}
4 changes: 1 addition & 3 deletions internal/context/pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ func NewPool[P any](maxCap int) *Pool[P] {
return &Pool[P]{
pool: sync.Pool{
New: func() any {
return &PooledSlice[P]{
Items: make([]P, 0, maxCap),
}
return &PooledSlice[P]{}
},
},
}
Expand Down
14 changes: 14 additions & 0 deletions internal/context/pools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package context

import "testing"

func TestPoolGetAllocatesToRequestedSizeOnFirstUse(t *testing.T) {
t.Parallel()

pool := NewPool[int](64)
items := pool.Get(1)

if cap(items.Items) != 1 {
t.Fatalf("cap(items.Items) = %d, want 1", cap(items.Items))
}
}
6 changes: 6 additions & 0 deletions internal/parameters/v18/accessor_scalars.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ func (p Parameter) StringValue() (string, bool) {
}

func (p Parameter) Float32Value() (float32, bool) {
if p.Kind == FloatZeroType {
return 0, true
}
if p.Kind != Float32Type {
return 0, false
}
return math.Float32frombits(uint32(p.Num)), true
}

func (p Parameter) Float64Value() (float64, bool) {
if p.Kind == DoubleZeroType {
return 0, true
}
if p.Kind != Float64Type {
return 0, false
}
Expand Down
2 changes: 1 addition & 1 deletion internal/parameters/v18/parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func scanPayload(reader *reader.Reader, dest *Value) error {
dest.Num = 1
case BooleanFalseType:
dest.Num = 0
case IntZeroType, ShortZeroType, LongZeroType, ByteZeroType:
case IntZeroType, ShortZeroType, LongZeroType, FloatZeroType, DoubleZeroType, ByteZeroType:
break
case ArrayType:
err = scanArray(reader, dest)
Expand Down
55 changes: 55 additions & 0 deletions internal/parameters/v18/scan_scalars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,61 @@ func TestParseFloat32ParameterAndAccessor(t *testing.T) {
}
}

func TestParseZeroFloatParametersAndAccessors(t *testing.T) {
tests := []struct {
name string
input []byte
wantID uint8
wantKind v18.ParameterType
wantCursor int
}{
{
name: "float32 zero shorthand",
input: []byte{0x01, byte(v18.FloatZeroType)},
wantID: 1,
wantKind: v18.FloatZeroType,
wantCursor: 2,
},
{
name: "float64 zero shorthand",
input: []byte{0x02, byte(v18.DoubleZeroType)},
wantID: 2,
wantKind: v18.DoubleZeroType,
wantCursor: 2,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := reader.NewReader(tt.input)
var parser v18.Parameter
var got v18.Parameter

if err := parser.ParseInto(r, nil, &got); err != nil {
t.Fatalf("Parse() error = %v", err)
}
if got.ID() != tt.wantID {
t.Errorf("ID() = %d, want %d", got.ID(), tt.wantID)
}
if got.Kind != tt.wantKind {
t.Errorf("Kind = %d, want %d", got.Kind, tt.wantKind)
}
if r.Cursor != tt.wantCursor {
t.Errorf("Cursor = %d, want %d", r.Cursor, tt.wantCursor)
}
if tt.wantKind == v18.FloatZeroType {
if value, ok := got.Float32Value(); !ok || value != 0 {
t.Errorf("Float32Value() = %v, %v; want 0, true", value, ok)
}
} else {
if value, ok := got.Float64Value(); !ok || value != 0 {
t.Errorf("Float64Value() = %v, %v; want 0, true", value, ok)
}
}
})
}
}

func TestParseInt8ParameterAndAccessor(t *testing.T) {
tests := []struct {
name string
Expand Down
4 changes: 3 additions & 1 deletion internal/parameters/v18/v18.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@ func (p Parameter) MarshalJSON() ([]byte, error) {
out.Decoded, _ = p.IntValue()
case StringType:
out.Decoded, _ = p.StringValue()
case Float32Type:
case Float32Type, FloatZeroType:
out.Decoded, _ = p.Float32Value()
case Float64Type, DoubleZeroType:
out.Decoded, _ = p.Float64Value()
case BooleanType:
out.Decoded, _ = p.BooleanValue()
case Float32ArrayType:
Expand Down
7 changes: 4 additions & 3 deletions internal/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func ParseInto[P types.ParameterView](ctx *context.Context[P], dest *types.Sessi
return err
}

if dest.Commands[i].Type > types.SendReliableFragmentCommand {
if !types.IsKnownCommandType(dest.Commands[i].Type) {
break
}
}
Expand Down Expand Up @@ -90,7 +90,8 @@ func emit[P types.ParameterView](hooks *hooks.Hooks[P], dest *types.Session[P])
hooks.SyncHooks.OnSession(*dest)
}

if hooks.AsyncHooks.OnSession == nil {
ch := hooks.AsyncHooks.OnSession
if ch == nil || len(ch) == cap(ch) {
return
}

Expand All @@ -105,7 +106,7 @@ func emit[P types.ParameterView](hooks *hooks.Hooks[P], dest *types.Session[P])
}

select {
case hooks.AsyncHooks.OnSession <- s:
case ch <- s:
default:
}
}
Loading
Loading