-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a buffer reader helper class that can be used to read buffers safely. #3471
Merged
bzbarsky-apple
merged 1 commit into
project-chip:master
from
bzbarsky-apple:little-endian-reader
Oct 27, 2020
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
* | ||
* Copyright (c) 2020 Project CHIP Authors | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
/** | ||
* @file | ||
* Utility classes for safely reading from size-limited buffers. | ||
*/ | ||
|
||
#pragma once | ||
|
||
#include <climits> | ||
#include <lib/core/CHIPEncoding.h> | ||
#include <lib/core/CHIPError.h> | ||
#include <lib/support/CodeUtils.h> | ||
#include <stdint.h> | ||
|
||
namespace chip { | ||
namespace Encoding { | ||
namespace LittleEndian { | ||
|
||
/** | ||
* @class Reader | ||
* | ||
* Simple reader for reading little-endian things out of buffers. | ||
*/ | ||
class Reader | ||
{ | ||
public: | ||
/** | ||
* Create a data model reader from a given buffer and length. | ||
* | ||
* @param buffer The octet buffer to read from. The caller must ensure | ||
* (most simply by allocating the reader on the stack) that | ||
* the buffer outlives the reader. The buffer is allowed to | ||
* be null if buf_len is 0. | ||
* @param buf_len The number of octets in the buffer. | ||
*/ | ||
Reader(const uint8_t * buffer, uint16_t buf_len) : mBufStart(buffer), mReadPtr(buffer), mAvailable(buf_len) {} | ||
|
||
/** | ||
* Number of octets we have read so far. This might be able to go away once | ||
* we do less switching back and forth between DataModelReader and raw | ||
* buffers. | ||
*/ | ||
uint16_t OctetsRead() const { return static_cast<uint16_t>(mReadPtr - mBufStart); } | ||
|
||
/** | ||
* Read a single 8-bit unsigned integer. | ||
* | ||
* @param [out] dest Where the 8-bit integer goes. | ||
* | ||
* @return Whether the read succeeded. The read can fail if there are not | ||
* enough octets available. | ||
*/ | ||
CHECK_RETURN_VALUE | ||
CHIP_ERROR Read8(uint8_t * dest) { return Read(dest); } | ||
|
||
/** | ||
* Read a single 16-bit unsigned integer. | ||
* | ||
* @param [out] dest Where the 16-bit integer goes. | ||
* | ||
* @return Whether the read succeeded. The read can fail if there are not | ||
* enough octets available. | ||
*/ | ||
CHECK_RETURN_VALUE | ||
CHIP_ERROR Read16(uint16_t * dest) { return Read(dest); } | ||
|
||
/** | ||
* Read a single 32-bit unsigned integer. | ||
* | ||
* @param [out] dest Where the 32-bit integer goes. | ||
* | ||
* @return Whether the read succeeded. The read can fail if there are not | ||
* enough octets available. | ||
*/ | ||
CHECK_RETURN_VALUE | ||
CHIP_ERROR Read32(uint32_t * dest) { return Read(dest); } | ||
|
||
/** | ||
* Read a single 64-bit unsigned integer. | ||
* | ||
* @param [out] dest Where the 64-bit integer goes. | ||
* | ||
* @return Whether the read succeeded. The read can fail if there are not | ||
* enough octets available. | ||
*/ | ||
CHECK_RETURN_VALUE | ||
CHIP_ERROR Read64(uint64_t * dest) { return Read(dest); } | ||
|
||
protected: | ||
template <typename T> | ||
CHIP_ERROR Read(T * retval) | ||
{ | ||
static_assert(CHAR_BIT == 8, "Our various sizeof checks rely on bytes and octets being the same thing"); | ||
|
||
static constexpr size_t data_size = sizeof(T); | ||
|
||
if (mAvailable < data_size) | ||
{ | ||
return CHIP_ERROR_BUFFER_TOO_SMALL; | ||
} | ||
|
||
Read(mReadPtr, retval); | ||
mAvailable = static_cast<uint16_t>(mAvailable - data_size); | ||
return CHIP_NO_ERROR; | ||
} | ||
|
||
private: | ||
// These helper methods return void and put the value being read into an | ||
// outparam because that allows us to easily overload on the type of the | ||
// thing being read. | ||
void Read(const uint8_t *& p, uint8_t * dest) { *dest = Encoding::Read8(p); } | ||
void Read(const uint8_t *& p, uint16_t * dest) { *dest = Encoding::LittleEndian::Read16(p); } | ||
void Read(const uint8_t *& p, uint32_t * dest) { *dest = Encoding::LittleEndian::Read32(p); } | ||
void Read(const uint8_t *& p, uint64_t * dest) { *dest = Encoding::LittleEndian::Read64(p); } | ||
|
||
/** | ||
* Our buffer start. | ||
*/ | ||
const uint8_t * const mBufStart; | ||
|
||
/** | ||
* Our current read point. | ||
*/ | ||
const uint8_t * mReadPtr; | ||
|
||
/** | ||
* The number of octets we can still read starting at mReadPtr. | ||
*/ | ||
uint16_t mAvailable; | ||
}; | ||
|
||
} // namespace LittleEndian | ||
} // namespace Encoding | ||
} // namespace chip |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: should we somehow try to make errors persistent? If I have (wrong really) code like:
we do not notice that a value failed to be read in between. I imagine we generally want to either use some GetLength() call to figure out available length (if data is variable size) or once reader failed to read, it became invalid.
If we have persistent errors, we can have builder patters:
instead of a more verbose VerifyOrExit:
I could have similar
SuccessOrExit(reader.StatusCode())
if I wanted to for some performance benefits, however I generally expect happy path to almost alwyas happen and would be willing to trade some ifs cost on failure for smaller flash usage.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That code won't compile, because
Read32
is marked asCHECK_RETURN_VALUE
and in this code that return value is being ignored.You could have code like this, though:
but hopefully at that point you would notice that you are not doing anything with
res
. And even better would be if we could make the compiler notice...That does make sense, in cases when all the reads are unconditional. In cases when the reads are conditional on the value a previous read returned, people would need to make sure to examine the success value of the previous read. I guess we could maybe do that if we require that the reference we return will be used (for either an immediate unconditional read call or for an error check)?
One other note: I plan to build a data-model-specific reader on top of this one to address #2168. But I suspect that might also be possible with the reader pattern. Will check.
I'll prototype out this suggestion and see how the code and the resulting codesize look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andy31415 OK, I put up some example code at https://github.com/bzbarsky-apple/connectedhomeip/tree/little-endian-reader-builder and https://github.com/bzbarsky-apple/connectedhomeip/tree/data-model-message-reader-builder for the builder pattern approach. It seems to use more codesize than the code I had before, though maybe it would do a bit better with some more out-of-lining, since
Read
ends up with a bit more logic.Fundamentally, the builder setup still ends up doing the "did the previous call succeed" on every read check, which is what the
SuccessOrExit
was doing...I did test instead setting
mAvailable
to 0 on read failure, so we don't have the extra branch on the status, but the end result is the same in terms of codesize; presumably the assignment is about the same size as the status-check branch.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can iterate. I would expect it to be smaller in size, but only if used a lot (so that the save of no extra if is worth it to extra logic inside reads).
I am ok with the PR as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that works. I'm pretty happy to revisit once we have more consumers and can get better size data. Converting should not be that big a problem, I suspect....
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some more experimenting, and I think you're right and the builder pattern might be a better fit here. Filed #3495 and will put up a PR soon.