Skip to content

Commit 52b2182

Browse files
committed
feat: Enhance server capabilities with new configuration options
- Added `guid`, `tcp_port`, `advertise_ip`, and `listen_ip` fields to `ServerState` struct for improved server identification and configuration. - Updated `handle_connection` function to utilize new server configuration options during TCP search responses. - Modified server initialization to generate and store a unique GUID. - Refactored connection validation to support multiple user authentication methods. - Improved handling of unsupported commands, specifically `AUTHNZ`, to ensure compatibility with existing clients. - Reinstated the old and added new interop tests for p4p client operations against the spvirit server, ensuring robust functionality across different PV types. - Updated various test scripts and binaries to reflect the new naming conventions for server tools (e.g., `spserver`, `spput`, `spget`). - Enhanced resilience of search operations and improved error handling in protocol tests.
1 parent d87d9a0 commit 52b2182

18 files changed

Lines changed: 881 additions & 113 deletions

.github/workflows/ci.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
build-and-test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Install Rust toolchain
19+
uses: dtolnay/rust-toolchain@stable
20+
21+
- name: Cache cargo registry and build
22+
uses: actions/cache@v4
23+
with:
24+
path: |
25+
~/.cargo/registry
26+
~/.cargo/git
27+
target
28+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
29+
restore-keys: |
30+
${{ runner.os }}-cargo-
31+
32+
- name: Build (release)
33+
run: cargo build --release
34+
35+
- name: Run tests
36+
run: cargo test --all
37+
38+
p4p-interop:
39+
runs-on: ubuntu-latest
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Install Rust toolchain
44+
uses: dtolnay/rust-toolchain@stable
45+
46+
- name: Cache cargo registry and build
47+
uses: actions/cache@v4
48+
with:
49+
path: |
50+
~/.cargo/registry
51+
~/.cargo/git
52+
target
53+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
54+
restore-keys: |
55+
${{ runner.os }}-cargo-
56+
57+
- name: Set up Python
58+
uses: actions/setup-python@v5
59+
with:
60+
python-version: "3.12"
61+
62+
- name: Install p4p
63+
run: pip install p4p
64+
65+
- name: Run p4p interop tests
66+
env:
67+
PVA_TEST_P4P: "1"
68+
P4P_PROVIDER_CMD: "python3 ${{ github.workspace }}/spvirit-tools/tests/interop/p4p_server.py"
69+
P4P_PROVIDER_READY_MS: "5000"
70+
run: cargo test --all

spvirit-codec/src/epics_decode.rs

Lines changed: 115 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ pub struct PvaConnectionValidationPayload {
493493

494494
impl PvaConnectionValidationPayload {
495495
pub fn new(raw: &[u8], is_be: bool, is_server: bool) -> Option<Self> {
496-
if raw.len() < 8 {
496+
if raw.len() < 6 {
497497
debug!(
498498
"PvaConnectionValidationPayload::new: raw too short {}",
499499
raw.len()
@@ -510,44 +510,72 @@ impl PvaConnectionValidationPayload {
510510
} else {
511511
u16::from_le_bytes(raw[4..6].try_into().ok()?)
512512
};
513-
let qos = if is_be {
514-
u16::from_be_bytes(raw[6..8].try_into().ok()?)
515-
} else {
516-
u16::from_le_bytes(raw[6..8].try_into().ok()?)
517-
};
518-
let authz = if raw.len() > 8 {
519-
// Try legacy format: single string after qos.
520-
if let Some((s, consumed)) = decode_string(&raw[8..], is_be) {
521-
if 8 + consumed == raw.len() {
522-
Some(s)
523-
} else {
524-
// AuthZ flags + name + method (spec-style).
525-
let mut offset = 9; // skip flags
526-
let name = decode_string(&raw[offset..], is_be).map(|(s, c)| {
527-
offset += c;
528-
s
529-
});
530-
let method = decode_string(&raw[offset..], is_be).map(|(s, _)| s);
531-
match (name, method) {
532-
(Some(n), _) if !n.is_empty() => Some(n),
533-
(_, Some(m)) if !m.is_empty() => Some(m),
534-
_ => None,
513+
514+
if is_server {
515+
// Server→client: buffer_size(u32) + isize(u16) + Size(nauth) + nauth × string
516+
// No QoS field.
517+
let mut offset = 6;
518+
let authz = if offset < raw.len() {
519+
if let Some((count, consumed)) = decode_size(&raw[offset..], is_be) {
520+
offset += consumed;
521+
let mut first_method = None;
522+
for _ in 0..count {
523+
if let Some((s, c)) = decode_string(&raw[offset..], is_be) {
524+
if first_method.is_none() && !s.is_empty() {
525+
first_method = Some(s);
526+
}
527+
offset += c;
528+
}
535529
}
530+
first_method
531+
} else {
532+
// Fallback: try single string (legacy spvirit servers).
533+
decode_string(&raw[offset..], is_be).map(|(s, _)| s)
536534
}
537535
} else {
538536
None
539-
}
537+
};
538+
539+
Some(Self {
540+
is_server,
541+
buffer_size,
542+
introspection_registry_size,
543+
qos: 0,
544+
authz,
545+
})
540546
} else {
541-
None
542-
};
547+
// Client→server: buffer_size(u32) + isize(u16) + qos(u16) + auth_method(string) [+ FieldDesc cred]
548+
if raw.len() < 8 {
549+
return None;
550+
}
551+
let qos = if is_be {
552+
u16::from_be_bytes(raw[6..8].try_into().ok()?)
553+
} else {
554+
u16::from_le_bytes(raw[6..8].try_into().ok()?)
555+
};
556+
let authz = if raw.len() > 8 {
557+
if let Some((s, consumed)) = decode_string(&raw[8..], is_be) {
558+
if 8 + consumed == raw.len() {
559+
Some(s)
560+
} else {
561+
// Has trailing FieldDesc for credentials; auth name is the string.
562+
Some(s)
563+
}
564+
} else {
565+
None
566+
}
567+
} else {
568+
None
569+
};
543570

544-
Some(Self {
545-
is_server,
546-
buffer_size,
547-
introspection_registry_size,
548-
qos,
549-
authz,
550-
})
571+
Some(Self {
572+
is_server,
573+
buffer_size,
574+
introspection_registry_size,
575+
qos,
576+
authz,
577+
})
578+
}
551579
}
552580
}
553581

@@ -726,22 +754,58 @@ impl PvaGetFieldPayload {
726754

727755
#[derive(Debug)]
728756
pub struct PvaMessagePayload {
757+
pub ioid: u32,
758+
pub message_type: u8,
759+
pub message: Option<String>,
760+
/// Legacy compat: if the payload looks like old Status format, decode that.
729761
pub status: Option<PvaStatus>,
730762
pub raw: Vec<u8>,
731763
}
732764

733765
impl PvaMessagePayload {
734766
pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
735-
let (status, consumed) = decode_status(raw, is_be);
736-
let remainder = if raw.len() > consumed {
737-
raw[consumed..].to_vec()
767+
// PVA spec MESSAGE format: ioid(u32) + message_type(u8) + message(string)
768+
if raw.len() >= 5 {
769+
let ioid = if is_be {
770+
u32::from_be_bytes(raw[0..4].try_into().ok()?)
771+
} else {
772+
u32::from_le_bytes(raw[0..4].try_into().ok()?)
773+
};
774+
let message_type = raw[4];
775+
let message = if raw.len() > 5 {
776+
decode_string(&raw[5..], is_be).map(|(s, _)| s)
777+
} else {
778+
None
779+
};
780+
// Build a synthetic PvaStatus so existing tests/code that inspect .status still work.
781+
let code = match message_type {
782+
0 => 0xFF, // info → OK
783+
1 => 0x01, // warning
784+
2 => 0x02, // error
785+
_ => 0x03, // fatal
786+
};
787+
let status = Some(PvaStatus {
788+
code,
789+
message: message.clone(),
790+
stack: None,
791+
});
792+
Some(Self {
793+
ioid,
794+
message_type,
795+
message,
796+
status,
797+
raw: raw.to_vec(),
798+
})
738799
} else {
739-
vec![]
740-
};
741-
Some(Self {
742-
status,
743-
raw: remainder,
744-
})
800+
// Fallback for very short payloads
801+
Some(Self {
802+
ioid: 0,
803+
message_type: 0,
804+
message: None,
805+
status: None,
806+
raw: raw.to_vec(),
807+
})
808+
}
745809
}
746810
}
747811

@@ -1337,16 +1401,17 @@ impl PvaOpPayload {
13371401
};
13381402

13391403
// Status is only present in certain subcmd types:
1340-
// - INIT responses (subcmd & 0x08) from server
1341-
// - NOT present in data updates (subcmd == 0x00) - those start with bitset directly
1342-
// Status format (per Lua dissector): first byte = code. If code==0xff (255) -> no status, remaining buffer is PVD.
1343-
// Otherwise follow with two length-prefixed strings: message, stack.
1404+
// Status format (per Lua dissector): first byte = code. If code==0xff (255) -> OK
1405+
// shorthand (1 byte only). Otherwise follow with two length-prefixed strings:
1406+
// message, stack.
1407+
// Server responses carry a status prefix for INIT responses (subcmd & 0x08),
1408+
// and for non-INIT responses on GET (10), PUT (11), PUT_GET (12).
1409+
// Monitor (13) data updates (non-INIT) do NOT have a status prefix.
13441410
let mut status: Option<PvaStatus> = None;
13451411
let mut pvd_raw: Vec<u8> = vec![];
13461412

1347-
// Only parse status for INIT responses (subcmd & 0x08), not for data updates (subcmd=0x00).
1348-
// Some servers still prefix data responses with 0xFF status OK; handle that below.
1349-
let has_status = is_server && (subcmd & 0x08) != 0;
1413+
let has_status = is_server
1414+
&& ((subcmd & 0x08) != 0 || (command != 13 && command != 14));
13501415

13511416
if !body.is_empty() {
13521417
if has_status {
@@ -1358,13 +1423,7 @@ impl PvaOpPayload {
13581423
vec![]
13591424
};
13601425
} else {
1361-
// No status for data updates - body is the raw PVD (bitset + values).
1362-
// Some servers still prefix data responses with status OK (0xFF). Skip it.
1363-
if body[0] == 0xFF {
1364-
pvd_raw = body[1..].to_vec();
1365-
} else {
1366-
pvd_raw = body.clone();
1367-
}
1426+
pvd_raw = body.clone();
13681427
}
13691428
}
13701429

spvirit-codec/src/spvirit_encode.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ pub fn encode_status_fatal(message: &str, is_be: bool) -> Vec<u8> {
4848
}
4949

5050
pub fn encode_message_error(message: &str, version: u8, is_be: bool) -> Vec<u8> {
51-
let payload = encode_status_error(message, is_be);
51+
// MESSAGE (cmd=18) payload: ioid(u32) + message_type(u8) + message(string)
52+
// Use ioid=0 and message_type=2 (error).
53+
let mut payload = Vec::new();
54+
payload.extend_from_slice(&if is_be { 0u32.to_be_bytes() } else { 0u32.to_le_bytes() });
55+
payload.push(2); // error
56+
payload.extend_from_slice(&encode_string_pva(message, is_be));
5257
let mut out = encode_header(true, is_be, false, version, 18, payload.len() as u32);
5358
out.extend_from_slice(&payload);
5459
out
@@ -149,11 +154,14 @@ pub fn encode_control_message(
149154
pub fn encode_connection_validation(
150155
buffer_size: u32,
151156
introspection_registry_size: u16,
152-
qos: u16,
153-
authz_name: &str,
157+
auth_methods: &[&str],
154158
version: u8,
155159
is_be: bool,
156160
) -> Vec<u8> {
161+
// Server→client CONNECTION_VALIDATION (cmd=1):
162+
// buffer_size(u32) + introspection_registry_size(u16)
163+
// + Size(count) + count × string (auth method names)
164+
// NOTE: No QoS field in the server→client direction.
157165
let mut payload = Vec::new();
158166
payload.extend_from_slice(&if is_be {
159167
buffer_size.to_be_bytes()
@@ -165,12 +173,10 @@ pub fn encode_connection_validation(
165173
} else {
166174
introspection_registry_size.to_le_bytes()
167175
});
168-
payload.extend_from_slice(&if is_be {
169-
qos.to_be_bytes()
170-
} else {
171-
qos.to_le_bytes()
172-
});
173-
payload.extend_from_slice(&encode_string_pva(authz_name, is_be));
176+
payload.extend_from_slice(&encode_size_pva(auth_methods.len(), is_be));
177+
for method in auth_methods {
178+
payload.extend_from_slice(&encode_string_pva(method, is_be));
179+
}
174180
let mut out = encode_header(true, is_be, false, version, 1, payload.len() as u32);
175181
out.extend_from_slice(&payload);
176182
out
@@ -1009,15 +1015,16 @@ mod tests {
10091015

10101016
#[test]
10111017
fn encode_decode_connection_validation_roundtrip() {
1012-
let msg = encode_connection_validation(4096, 2, 0x10, "test", 2, true);
1018+
let msg = encode_connection_validation(4096, 2, &["anonymous", "ca"], 2, true);
10131019
let mut pkt = PvaPacket::new(&msg);
10141020
let cmd = pkt.decode_payload().expect("decoded");
10151021
match cmd {
10161022
PvaPacketCommand::ConnectionValidation(payload) => {
1023+
assert!(payload.is_server);
10171024
assert_eq!(payload.buffer_size, 4096);
10181025
assert_eq!(payload.introspection_registry_size, 2);
1019-
assert_eq!(payload.qos, 0x10);
1020-
assert_eq!(payload.authz.as_deref(), Some("test"));
1026+
assert_eq!(payload.qos, 0); // server→client has no qos
1027+
assert_eq!(payload.authz.as_deref(), Some("anonymous"));
10211028
}
10221029
other => panic!("unexpected decode: {:?}", other),
10231030
}

0 commit comments

Comments
 (0)