1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//! C-TAG (Customer VLAN Tag) Information Element
//!
//! The C-TAG IE contains customer VLAN tagging information for Ethernet packet filtering.
//! Per 3GPP TS 29.244 Section 8.2.94, this IE is used in Ethernet packet filtering scenarios.
//!
//! C-TAG consists of priority, DEI (Drop Eligible Indicator), and VID (VLAN ID).
use crate::error::PfcpError;
use crate::ie::{Ie, IeType};
/// C-TAG (Customer VLAN Tag)
///
/// Represents customer VLAN tagging information per IEEE 802.1Q.
///
/// # 3GPP Reference
/// 3GPP TS 29.244 Section 8.2.94
///
/// # Structure
/// - Octet 5: Flags (PCP Priority, DEI)
/// - Octet 5-6: VID (VLAN ID) - 12 bits
///
/// ```text
/// Bits 8-6: PCP (Priority Code Point) - 3 bits
/// Bit 5: DEI (Drop Eligible Indicator) - 1 bit
/// Bits 4-1 (and next 8 bits): VID (VLAN ID) - 12 bits
/// ```
///
/// # Examples
///
/// ```
/// use rs_pfcp::ie::c_tag::CTag;
///
/// // Create C-TAG with priority 3, no DEI, VLAN ID 100
/// let ctag = CTag::new(3, false, 100).unwrap();
/// assert_eq!(ctag.priority(), 3);
/// assert_eq!(ctag.dei(), false);
/// assert_eq!(ctag.vid(), 100);
///
/// // Marshal and unmarshal
/// let bytes = ctag.marshal();
/// let parsed = CTag::unmarshal(&bytes).unwrap();
/// assert_eq!(ctag, parsed);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CTag {
/// Priority Code Point (0-7, 3 bits)
pcp: u8,
/// Drop Eligible Indicator (boolean)
dei: bool,
/// VLAN ID (0-4095, 12 bits)
vid: u16,
}
impl CTag {
/// Maximum valid priority (3 bits = 0-7)
pub const MAX_PRIORITY: u8 = 7;
/// Maximum valid VID (12 bits = 0-4095)
pub const MAX_VID: u16 = 4095;
/// Create a new C-TAG
///
/// # Arguments
/// * `pcp` - Priority Code Point (0-7)
/// * `dei` - Drop Eligible Indicator
/// * `vid` - VLAN ID (0-4095)
///
/// # Errors
/// Returns error if PCP > 7 or VID > 4095
///
/// # Example
/// ```
/// use rs_pfcp::ie::c_tag::CTag;
///
/// let ctag = CTag::new(5, true, 1000).unwrap();
/// assert_eq!(ctag.priority(), 5);
/// assert_eq!(ctag.dei(), true);
/// assert_eq!(ctag.vid(), 1000);
/// ```
pub fn new(pcp: u8, dei: bool, vid: u16) -> Result<Self, PfcpError> {
if pcp > Self::MAX_PRIORITY {
return Err(PfcpError::invalid_value(
"C-TAG PCP",
pcp.to_string(),
format!("exceeds maximum {}", Self::MAX_PRIORITY),
));
}
if vid > Self::MAX_VID {
return Err(PfcpError::invalid_value(
"C-TAG VID",
vid.to_string(),
format!("exceeds maximum {}", Self::MAX_VID),
));
}
Ok(CTag { pcp, dei, vid })
}
/// Get the Priority Code Point
pub fn priority(&self) -> u8 {
self.pcp
}
/// Get the Drop Eligible Indicator
pub fn dei(&self) -> bool {
self.dei
}
/// Get the VLAN ID
pub fn vid(&self) -> u16 {
self.vid
}
/// Marshal C-TAG to bytes
///
/// # Returns
/// 3-byte array with C-TAG encoded as:
/// - Byte 0: PCP (bits 7-5) | DEI (bit 4) | VID high nibble (bits 3-0)
/// - Bytes 1-2: VID low byte
pub fn marshal(&self) -> [u8; 3] {
let mut bytes = [0u8; 3];
// Byte 0: PCP (3 bits) | DEI (1 bit) | VID[11:8] (4 bits)
bytes[0] = (self.pcp << 5) | ((self.dei as u8) << 4) | ((self.vid >> 8) as u8 & 0x0F);
// Byte 1: VID[7:0]
bytes[1] = (self.vid & 0xFF) as u8;
// Byte 2: Spare (0)
bytes[2] = 0;
bytes
}
/// Unmarshal C-TAG from bytes
///
/// # Arguments
/// * `data` - Byte slice containing C-TAG data (must be at least 3 bytes)
///
/// # Errors
/// Returns error if data is too short
///
/// # Example
/// ```
/// use rs_pfcp::ie::c_tag::CTag;
///
/// let ctag = CTag::new(7, false, 4095).unwrap();
/// let bytes = ctag.marshal();
/// let parsed = CTag::unmarshal(&bytes).unwrap();
/// assert_eq!(ctag, parsed);
/// ```
pub fn unmarshal(data: &[u8]) -> Result<Self, PfcpError> {
if data.len() < 3 {
return Err(PfcpError::invalid_length(
"C-TAG",
IeType::CTag,
3,
data.len(),
));
}
// Extract PCP (bits 7-5 of byte 0)
let pcp = (data[0] >> 5) & 0x07;
// Extract DEI (bit 4 of byte 0)
let dei = (data[0] & 0x10) != 0;
// Extract VID (bits 3-0 of byte 0 + byte 1)
let vid = (((data[0] & 0x0F) as u16) << 8) | (data[1] as u16);
Ok(CTag { pcp, dei, vid })
}
/// Convert to generic IE
///
/// # Example
/// ```
/// use rs_pfcp::ie::c_tag::CTag;
/// use rs_pfcp::ie::IeType;
///
/// let ctag = CTag::new(2, false, 500).unwrap();
/// let ie = ctag.to_ie();
/// assert_eq!(ie.ie_type, IeType::CTag);
/// ```
pub fn to_ie(&self) -> Ie {
Ie::new(IeType::CTag, self.marshal().to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ctag_new_valid() {
let ctag = CTag::new(0, false, 0).unwrap();
assert_eq!(ctag.priority(), 0);
assert!(!ctag.dei());
assert_eq!(ctag.vid(), 0);
let ctag2 = CTag::new(7, true, 4095).unwrap();
assert_eq!(ctag2.priority(), 7);
assert!(ctag2.dei());
assert_eq!(ctag2.vid(), 4095);
}
#[test]
fn test_ctag_new_invalid_pcp() {
let result = CTag::new(8, false, 100);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PfcpError::InvalidValue { .. }));
}
#[test]
fn test_ctag_new_invalid_vid() {
let result = CTag::new(3, false, 4096);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PfcpError::InvalidValue { .. }));
}
#[test]
fn test_ctag_marshal() {
let ctag = CTag::new(5, true, 1000).unwrap();
let bytes = ctag.marshal();
assert_eq!(bytes.len(), 3);
// Verify PCP (bits 7-5) = 5 (101)
// Verify DEI (bit 4) = 1
// Verify VID high nibble (bits 3-0) = 3 (from 1000 = 0x3E8)
// Expected byte 0: 101 1 0011 = 0xB3
assert_eq!(bytes[0], 0xB3);
// Verify VID low byte = 0xE8
assert_eq!(bytes[1], 0xE8);
// Spare byte
assert_eq!(bytes[2], 0x00);
}
#[test]
fn test_ctag_unmarshal_valid() {
let data = [0xB3, 0xE8, 0x00]; // PCP=5, DEI=1, VID=1000
let ctag = CTag::unmarshal(&data).unwrap();
assert_eq!(ctag.priority(), 5);
assert!(ctag.dei());
assert_eq!(ctag.vid(), 1000);
}
#[test]
fn test_ctag_unmarshal_short() {
let data = [0xB3, 0xE8];
let result = CTag::unmarshal(&data);
assert!(result.is_err());
}
#[test]
fn test_ctag_unmarshal_empty() {
let result = CTag::unmarshal(&[]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, PfcpError::InvalidLength { .. }));
}
#[test]
fn test_ctag_round_trip() {
let test_cases = vec![
(0, false, 0),
(7, true, 4095),
(3, false, 100),
(5, true, 1000),
(1, false, 1),
];
for (pcp, dei, vid) in test_cases {
let original = CTag::new(pcp, dei, vid).unwrap();
let marshaled = original.marshal();
let unmarshaled = CTag::unmarshal(&marshaled).unwrap();
assert_eq!(
original, unmarshaled,
"Failed for PCP={}, DEI={}, VID={}",
pcp, dei, vid
);
}
}
#[test]
fn test_ctag_to_ie() {
let ctag = CTag::new(4, false, 200).unwrap();
let ie = ctag.to_ie();
assert_eq!(ie.ie_type, IeType::CTag);
assert_eq!(ie.payload.len(), 3);
// Verify IE can be unmarshaled
let parsed = CTag::unmarshal(&ie.payload).unwrap();
assert_eq!(ctag, parsed);
}
#[test]
fn test_ctag_scenarios() {
// Scenario 1: Default VLAN (VID 1, no priority)
let default_vlan = CTag::new(0, false, 1).unwrap();
assert_eq!(default_vlan.vid(), 1);
assert_eq!(default_vlan.priority(), 0);
// Scenario 2: High priority voice traffic (PCP 6)
let voice = CTag::new(6, false, 100).unwrap();
assert_eq!(voice.priority(), 6);
// Scenario 3: Video traffic with DEI set
let video = CTag::new(5, true, 200).unwrap();
assert_eq!(video.priority(), 5);
assert!(video.dei());
// Scenario 4: Management VLAN
let mgmt = CTag::new(7, false, 10).unwrap();
assert_eq!(mgmt.vid(), 10);
assert_eq!(mgmt.priority(), 7);
// Scenario 5: Maximum values
let max_ctag = CTag::new(7, true, 4095).unwrap();
assert_eq!(max_ctag.priority(), 7);
assert!(max_ctag.dei());
assert_eq!(max_ctag.vid(), 4095);
}
#[test]
fn test_ctag_boundary_values() {
// Minimum values
assert!(CTag::new(0, false, 0).is_ok());
// Maximum values
assert!(CTag::new(7, true, 4095).is_ok());
// Out of range
assert!(CTag::new(8, false, 0).is_err());
assert!(CTag::new(0, false, 4096).is_err());
}
}