Differences in parsing UUIDs from uint128 in Python and Rust #406
Labels
No labels
breaking
bug
dependencies
good first issue
help wanted
unstable
No milestone
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
gh-uuid-rs/uuid#406
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Describe the bug
I'm creating a binding of this library to Python for learning purposes and possible production usage.
I've encountered a difference in the values Python provides and the values Rust provides when parsing a UUID from an unsigned 128-bit integer.
To Reproduce
When parsing a UUID from an unsigned integer in Python I get the following UUID:
However, when I use the following rust code:
The output is the following UUID:
2cc645a4-ce9e-6fb3-964f-24e99f8aafd7Expected behavior
I think that the value should be the same in both implementations.
If not, we need to figure out the difference and decide which implementation is correct.
Screenshots
If applicable, add screenshots to help explain your problem.
Specifications (please complete the following information):
Additional context
See the Python implementation here.
Other
N/A
cc @KodrAus
Looks like endianness? Will dig in properly in a bit, but maybe we’re reversing the bits in that 128bit number so they’re big endian whereas Python isn’t?
Now I feel silly... thats the first thing I ruled out :P
That's exactly the case.
I did the following:
and the tests pass. I'm not sure which implementation is correct (or if both are correct).
In any case, this should be documented.
Yeh, I think because a
uuiddoesn’t have a representation as a 128bit number any semantics we pick will be fairly arbitrary.I think we should consider deprecating the 128bit conversions.
I disagree.
It's useful at least to me.
@thedrow In that case you could define your own function in your bindings to Python with the semantics you need, though.
The problem is we’ve chosen one arbitrary representation where the 128bit number has a platform-specific endianness but the representation you want is for the 128bit number to act like a
[u8; 16]where there is no endianness.Since there are multiple equally valid approaches here I think we should consider punting supporting it.
I think a better way will be to provide another adapter
Integerthat handles theu128explicitly. I agree that conversion functionality is important. However as seen here, this introduces the issue of the endianness of theu128being ambiguous.The other thing that I was thinking last night was that we do not have any types that guarantee about the endianness of the bytes within. We could introduce a
adapter::endianmodule that has these. I am not a 100% sure if this would be the best fit for uuid crate itself thoughThe behavior here is not surprising, well, not to me at least. Literals are big endian and get converted to the target endianess. Given that most (all?) x86/x64 CPUs are little endian we end up with exactly that. Python – like the JVM – is probably normalizing exactly that so that users do not get confused.
There is
target_endianwith which it would be possible to select the right default implementation for theu128toUuidconversion at compile time. However, I cannot tell if targets exist that may have a different Endianess depending on the used CPU. I do not believe so but believing means knowing nothing.The – by far – safest choice to to simply call
to_beandto_lethat are no-op if the target is already using the expected Endianess and otherwise converts them. This way the Endianess is always correct and all operations placed on top are safe.Looking at the code I was wondering why there even is a
from_u128_lefn. Isn't rusts u128 defined as "big endian" from the pov of the developer (or maybe im just stupid...)? So the endianness conversion should happen at the "data reading" stage with for example from_le_bytes?All primitive numbers follow the target platform’s endianness... the from_le_bytes and from_be_bytes are noop if endian already matches, IIRC
yeah, im not talking about the native endianness the cpu is using, that is abstracted away by the compiler anyway. But the "conceptual"/"logical" endianness perceived by the developer.
Im just saying that the
from_le_bytesshould be used on a lower level (eg. while interpreting/parsing read data), and the higher level (Uuid::from_u128) should not bother with endianness.from_u128_leis mixing both, the lower level data endianness and the conceptual endianness.edit: "should" as "The existence of
from_u128_leis clashing with my understanding of the software/language design", not saying it should be removed.leexplicitly indicate little endian, since uuids are big endian by default as required by the specification. As such you don’t seebevariants. U128 specific one is used as u128 endianess is machine dependant.uuiditself does not implicitly handle endianess so the default behaviour is to assume the endianess of the inputted bytes(128 bits = 16 bytes) is correctly big endian.I think I'm still in favour of deprecating the
_lemethods entirely. They're a constant source of confusion. I think we should instead recommend callingswap_bytesyourself if you want to reverse the input fields. We can't really insert our own calls toto_beorto_lewithinuuidbecause that would break the existing semantics offrom_fields.If we did want to keep this functionality maybe it would be better called
from_fields_swapetc rather thanfrom_fields_lebecause I think it’s the endianness that’s making it confusing.Why do you say, that it's dependent on the native endianess?
is just the same on a little endianess machine and on a big endianess machine. There's no "endianess", because it gets abstracted away by the compiler. What matters here is the way how you're reading the data. And that's my argument, endianess should be handled on the reading side not in
uuid.If you're reading an u128 from a file and need to convert it in
uuidthen that u128 you have read was invalid, because your reading code is broken (returning data in the wrong endianess).I don't find the API confusing and it is very useful for my fastuuid library.
I'd rather keep the API as it is, if you don't mind.
This is where the confusion is I think. I've added some docs to our
mainbranch that call out the endianness we're talking about is within the UUID (where the fields are always big endian), rather than the machine, which may be either big or little endian. So when we're talking about little endian integers in the_lemethods, we're talking about integer fields encoded as little endian coming from some other source (like a Microsoft GUID) that need to be flipped for an equivalent UUID. I'm feeling less worried about deprecating them after that.I totally agree that handling any endianness at the read side probably makes the most sense if you're encoding them, but am not sure that means we'd need a
from_bytes_lemethod. If you're encoding a UUID as its raw bytes then either:as_fields, in which case you'd probably need to read those fields back out individually to re-interpret them together as a UUID usingfrom_fields.The
u128methods specifically are a little odd, because there's no specified interpretation of a UUID as a 128bit number, but to keep the behavior consistent with other methods that accept or produce integers, we have a default big-endian interpretation and an unconditionally flipped little-endian one. Since we flip the bytes in whole integers, that means we flip the whole number and not just the fields encoded in it.I'm just coming through some triage. I think the specific issue here is "resolved" as use
Uuid::from_u128_leif you want to construct aUuidfrom a 128bit number and flip it. It can be a little surprising, but the docs now try to call out why it's done this way. The_lemethods aren't going anywhere.I'll go ahead and close this one for now, but please feel free to re-open or create new issues for anything else that comes up!