Skip to content

Commit a9bac58

Browse files
committed
[thread-direct] LTV codec and Thread Header IE LTV layer
Thread Direct frames carry typed fields (SCA schedule, challenge nonce, target ID) inside a Thread Header IE. Standard TLV encoding places Type first, then Length — 2 bytes of fixed overhead per entry. For the small payloads here that cost is disproportionate. Instead, Thread Direct uses LTV (Length-Type-Value): length information leads each entry, and the two header fields share a single byte by splitting it adaptively. The split is determined by L — the remaining byte count at that position. As L shrinks, fewer bits are needed to represent it, freeing more bits for the type field. A decoder reading a stream position knows L and can recover both length and type from one byte. The all-ones pattern in the lower bits serves as an escape; a second header byte follows when needed. This commit introduces two layers: ltvs.hpp/cpp — generic LTV framework - Ltv: in-memory accessor and FrameBuilder append helpers. - SimpleLtvInfo<kType, T>: compile-time type-code/struct pairing so call sites write Ltv::Append<ChallengeLtvInfo>(builder, val) without repeating the type constant. - PackedLtvStream: Encode, Decode, and a forward Iterator with GetValueOffset() for ISR-safe in-place field updates (eg. stamping the current SLW phase into a pre-built frame without repacking). mac_header_ltv.hpp/cpp — Thread Header IE LTV layer - ThreadHeaderIe (new in mac_header_ie.hpp): Element ID 0x2d and the three LTV type codes (TargetId, SCA, Challenge). - ScaParams: in-memory representation of the Scheduled Channel Access LTV (SLW period/phase, RAM offset/duration/bitmap, slot duration code). - ChallengeLtv/ChallengeLtvInfo: 16-byte HMAC-SHA256 challenge nonce. - FrameBuilder-based encoders and single-pass decoder over a packed stream.
1 parent 593fc53 commit a9bac58

9 files changed

Lines changed: 2044 additions & 0 deletions

File tree

src/core/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ set(COMMON_SOURCES
122122
common/frame_builder.cpp
123123
common/frame_data.cpp
124124
common/heap.cpp
125+
common/ltvs.cpp
125126
common/heap_data.cpp
126127
common/heap_string.cpp
127128
common/log.cpp
@@ -157,6 +158,7 @@ set(COMMON_SOURCES
157158
mac/mac_filter.cpp
158159
mac/mac_frame.cpp
159160
mac/mac_header_ie.cpp
161+
mac/mac_header_ltv.cpp
160162
mac/mac_links.cpp
161163
mac/mac_types.cpp
162164
mac/scan_result.cpp
@@ -316,6 +318,7 @@ set(RADIO_COMMON_SOURCES
316318
common/error.cpp
317319
common/frame_builder.cpp
318320
common/log.cpp
321+
common/ltvs.cpp
319322
common/random.cpp
320323
common/string.cpp
321324
common/tasklet.cpp
@@ -331,6 +334,7 @@ set(RADIO_COMMON_SOURCES
331334
mac/link_raw.cpp
332335
mac/mac_frame.cpp
333336
mac/mac_header_ie.cpp
337+
mac/mac_header_ltv.cpp
334338
mac/mac_types.cpp
335339
mac/sub_mac.cpp
336340
mac/sub_mac_callbacks.cpp

src/core/common/ltvs.cpp

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
/*
2+
* Copyright (c) 2025, The OpenThread Authors.
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
* 1. Redistributions of source code must retain the above copyright
8+
* notice, this list of conditions and the following disclaimer.
9+
* 2. Redistributions in binary form must reproduce the above copyright
10+
* notice, this list of conditions and the following disclaimer in the
11+
* documentation and/or other materials provided with the distribution.
12+
* 3. Neither the name of the copyright holder nor the
13+
* names of its contributors may be used to endorse or promote products
14+
* derived from this software without specific prior written permission.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
/**
30+
* @file
31+
* This file implements LTV (Length-Type-Value) generation and parsing.
32+
*/
33+
34+
#include "ltvs.hpp"
35+
36+
#include <string.h>
37+
38+
#include "common/code_utils.hpp"
39+
40+
namespace ot {
41+
42+
// Minimum number of bits needed to represent [0, aL-1]: smallest n with 2^n >= aL.
43+
static uint8_t BitsFor(uint8_t aL)
44+
{
45+
uint8_t n = 0;
46+
47+
while ((1u << n) < aL)
48+
{
49+
n++;
50+
}
51+
52+
return n;
53+
}
54+
55+
Error Ltv::AppendTo(FrameBuilder &aFrameBuilder) const { return aFrameBuilder.AppendBytes(this, GetTotalSize()); }
56+
57+
Error Ltv::Append(FrameBuilder &aFrameBuilder, uint8_t aType, const void *aValue, uint8_t aLength)
58+
{
59+
Error error;
60+
Ltv ltv;
61+
62+
ltv.SetLength(aLength);
63+
ltv.SetType(aType);
64+
SuccessOrExit(error = aFrameBuilder.AppendBytes(&ltv, kHeaderSize));
65+
66+
if (aLength > 0)
67+
{
68+
SuccessOrExit(error = aFrameBuilder.AppendBytes(aValue, aLength));
69+
}
70+
71+
exit:
72+
return error;
73+
}
74+
75+
const Ltv *Ltv::FindLtv(const void *aBuffer, uint16_t aLength, uint8_t aType)
76+
{
77+
Iterator iter;
78+
const Ltv *result = nullptr;
79+
80+
iter.Init(static_cast<const uint8_t *>(aBuffer), aLength);
81+
82+
while (!iter.IsDone())
83+
{
84+
const Ltv &ltv = iter.GetLtv();
85+
86+
// Advance first: validates the declared length fits in the buffer.
87+
VerifyOrExit(iter.Advance() == kErrorNone);
88+
89+
if (ltv.GetType() == aType)
90+
{
91+
result = &ltv;
92+
ExitNow();
93+
}
94+
}
95+
96+
exit:
97+
return result;
98+
}
99+
100+
void Ltv::Iterator::Init(const uint8_t *aBuffer, uint16_t aLength) { mData.Init(aBuffer, aLength); }
101+
102+
Error Ltv::Iterator::Advance(void)
103+
{
104+
Error error = kErrorNone;
105+
uint16_t size;
106+
107+
VerifyOrExit(!IsDone());
108+
109+
size = GetLtv().GetTotalSize();
110+
VerifyOrExit(mData.CanRead(size), error = kErrorParse);
111+
mData.SkipOver(size);
112+
113+
exit:
114+
return error;
115+
}
116+
117+
uint8_t PackedLtvStream::Encode(const uint8_t *aPlain, uint8_t aPlainLen, uint8_t *aPacked, uint8_t aPackedMax)
118+
{
119+
// Packed entries are encoded back-to-front because the header bit-split for each entry
120+
// depends on L = (own encoded size) + (all subsequent entries' encoded size). Collecting
121+
// entries first and then iterating in reverse lets us track `scratchLen` as we go.
122+
123+
static constexpr uint8_t kMaxEntries = 8;
124+
125+
struct Entry
126+
{
127+
uint8_t mType;
128+
uint8_t mLen;
129+
const uint8_t *mValue;
130+
};
131+
132+
Entry entries[kMaxEntries];
133+
uint8_t count = 0;
134+
135+
for (const uint8_t *p = aPlain, *end = aPlain + aPlainLen; p + 2 <= end && count < kMaxEntries; p += 2 + p[0])
136+
{
137+
entries[count].mLen = p[0];
138+
entries[count].mType = p[1];
139+
entries[count].mValue = p + 2;
140+
count++;
141+
}
142+
143+
uint8_t scratch[128]; // packed output never exceeds plain input; 128 covers max IE payload (127 bytes).
144+
uint8_t scratchLen = 0;
145+
146+
for (int i = static_cast<int>(count) - 1; i >= 0; i--)
147+
{
148+
const Entry &e = entries[i];
149+
uint8_t header[2];
150+
uint8_t hdrBytes = 0;
151+
152+
for (uint8_t hdr = 1; hdr <= 2; hdr++)
153+
{
154+
uint8_t L = static_cast<uint8_t>(hdr + e.mLen + scratchLen);
155+
uint8_t n = BitsFor(L);
156+
uint8_t shift = static_cast<uint8_t>(8u - n);
157+
158+
if (n == 0)
159+
{
160+
// L == 1: the sole byte is a bare type with implicit zero length.
161+
if (hdr == 1 && e.mLen == 0)
162+
{
163+
header[0] = e.mType;
164+
hdrBytes = 1;
165+
}
166+
167+
continue;
168+
}
169+
170+
if (e.mLen >= (1u << n))
171+
{
172+
continue;
173+
}
174+
175+
uint8_t allOnes = static_cast<uint8_t>((1u << shift) - 1u);
176+
177+
if (hdr == 1 && e.mType < allOnes)
178+
{
179+
header[0] = static_cast<uint8_t>((e.mLen << shift) | e.mType);
180+
hdrBytes = 1;
181+
break;
182+
}
183+
184+
if (hdr == 2)
185+
{
186+
// All-ones in the type field is the escape code; type follows in the next byte.
187+
header[0] = static_cast<uint8_t>((e.mLen << shift) | allOnes);
188+
header[1] = e.mType;
189+
hdrBytes = 2;
190+
break;
191+
}
192+
}
193+
194+
if (hdrBytes == 0)
195+
{
196+
continue;
197+
}
198+
199+
uint8_t entryLen = static_cast<uint8_t>(hdrBytes + e.mLen);
200+
201+
memmove(scratch + entryLen, scratch, scratchLen);
202+
memcpy(scratch, header, hdrBytes);
203+
204+
if (e.mLen > 0)
205+
{
206+
memcpy(scratch + hdrBytes, e.mValue, e.mLen);
207+
}
208+
209+
scratchLen += entryLen;
210+
}
211+
212+
uint8_t written = (scratchLen <= aPackedMax) ? scratchLen : aPackedMax;
213+
214+
memcpy(aPacked, scratch, written);
215+
216+
return written;
217+
}
218+
219+
Error PackedLtvStream::Decode(const uint8_t *aPacked,
220+
uint8_t aPackedLen,
221+
uint8_t *aPlain,
222+
uint8_t aPlainMax,
223+
uint8_t &aPlainLen)
224+
{
225+
Error error = kErrorNone;
226+
uint8_t outLen = 0;
227+
Iterator iter;
228+
229+
for (iter.Init(aPacked, aPackedLen); !iter.IsDone();)
230+
{
231+
uint8_t len = iter.GetLength();
232+
233+
VerifyOrExit(outLen + Ltv::kHeaderSize + len <= aPlainMax, error = kErrorNoBufs);
234+
aPlain[outLen++] = len;
235+
aPlain[outLen++] = iter.GetType();
236+
237+
if (len > 0)
238+
{
239+
memcpy(aPlain + outLen, iter.GetValue(), len);
240+
outLen += len;
241+
}
242+
243+
VerifyOrExit(iter.Advance() == kErrorNone, error = kErrorParse);
244+
}
245+
246+
aPlainLen = outLen;
247+
248+
exit:
249+
return error;
250+
}
251+
252+
void PackedLtvStream::Iterator::Init(const uint8_t *aPacked, uint8_t aPackedLen)
253+
{
254+
mBuffer = aPacked;
255+
mTotalLen = aPackedLen;
256+
mOffset = 0;
257+
mDone = false;
258+
mType = 0;
259+
mLen = 0;
260+
mValue = nullptr;
261+
mHdrSize = 0;
262+
IgnoreError(DecodeEntry());
263+
}
264+
265+
Error PackedLtvStream::Iterator::Advance(void)
266+
{
267+
Error error = kErrorNone;
268+
269+
VerifyOrExit(!mDone);
270+
mOffset = static_cast<uint8_t>(mOffset + mHdrSize + mLen);
271+
error = DecodeEntry();
272+
273+
exit:
274+
return error;
275+
}
276+
277+
Error PackedLtvStream::Iterator::DecodeEntry(void)
278+
{
279+
Error error = kErrorNone;
280+
uint8_t L;
281+
uint8_t n;
282+
uint8_t first;
283+
284+
if (mOffset >= mTotalLen)
285+
{
286+
mDone = true;
287+
ExitNow();
288+
}
289+
290+
L = static_cast<uint8_t>(mTotalLen - mOffset);
291+
n = BitsFor(L);
292+
first = mBuffer[mOffset];
293+
294+
if (n == 0)
295+
{
296+
mType = first;
297+
mLen = 0;
298+
mHdrSize = 1;
299+
}
300+
else
301+
{
302+
uint8_t shift = static_cast<uint8_t>(8u - n);
303+
uint8_t lenbits = static_cast<uint8_t>(first >> shift);
304+
uint8_t typebits = static_cast<uint8_t>(first & ((1u << shift) - 1u));
305+
306+
if (typebits == static_cast<uint8_t>((1u << shift) - 1u))
307+
{
308+
VerifyOrExit(mOffset + 1u < mTotalLen, error = kErrorParse; mDone = true);
309+
mType = mBuffer[mOffset + 1u];
310+
mLen = lenbits;
311+
mHdrSize = 2;
312+
}
313+
else
314+
{
315+
mType = typebits;
316+
mLen = lenbits;
317+
mHdrSize = 1;
318+
}
319+
}
320+
321+
VerifyOrExit(mOffset + mHdrSize + mLen <= mTotalLen, error = kErrorParse; mDone = true);
322+
mValue = mBuffer + mOffset + mHdrSize;
323+
324+
exit:
325+
return error;
326+
}
327+
328+
} // namespace ot

0 commit comments

Comments
 (0)