Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 additions & 12 deletions src/core/IronPython.Modules/_struct.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,19 +425,39 @@ public void pack_into(CodeContext/*!*/ context, [NotNone] IBufferProtocol/*!*/ b

[Documentation("reads the current format from the specified array")]
public PythonTuple/*!*/ unpack_from(CodeContext/*!*/ context, [BytesLike][NotNone] IList<byte>/*!*/ buffer, int offset = 0) {
int bytesAvail = buffer.Count - offset;
if (bytesAvail < size) {
offset = NormalizeOffset(context, offset, buffer.Count);
return unpack(context, buffer.Substring(offset, size));
}

[Documentation("reads the current format from the specified array")]
public PythonTuple/*!*/ unpack_from(CodeContext/*!*/ context, [NotNone] IBufferProtocol/*!*/ buffer, int offset = 0) {
using var buf = buffer.GetBuffer(BufferFlags.Simple);
var span = buf.AsReadOnlySpan();
offset = NormalizeOffset(context, offset, span.Length);
return unpack(context, span.Slice(offset, size).ToArray());
}

private int NormalizeOffset(CodeContext context, int offset, int length) {
if (offset < 0) {
offset += length;
}
int bytesAvail = length - offset;
if (offset < 0 || bytesAvail < size) {
throw Error(context, $"unpack_from requires a buffer of at least {size} bytes");
}

return unpack(context, buffer.Substring(offset, size));
return offset;
}

[Documentation("iteratively unpack the current format from the specified array.")]
public PythonUnpackIterator iter_unpack(CodeContext/*!*/ context, [BytesLike][NotNone] IList<byte>/*!*/ buffer) {
return new PythonUnpackIterator(this, context, buffer);
}

[Documentation("iteratively unpack the current format from the specified array.")]
public PythonUnpackIterator iter_unpack(CodeContext/*!*/ context, [NotNone] IBufferProtocol/*!*/ buffer) {
return new PythonUnpackIterator(this, context, buffer.GetBuffer(BufferFlags.Simple));
}

[Documentation("gets the number of bytes that the serialized string will occupy or are required to deserialize the data")]
public int size {
get {
Expand Down Expand Up @@ -676,17 +696,21 @@ internal static Struct Create(string/*!*/ format) {
#endregion
}

#nullable enable

[PythonType("unpack_iterator"), Documentation("Represents an iterator returned by _struct.iter_unpack()")]
public sealed class PythonUnpackIterator : IEnumerator<object>, IEnumerable<object> {
private object _iter_current;
private object? _iter_current;
private int _next_offset;

private readonly CodeContext _context;
private readonly IList<byte> _buffer;
private readonly IList<byte>? _buffer;
private readonly IPythonBuffer? _pythonBuffer;
private readonly Struct _owner;

internal PythonUnpackIterator(Struct/*!*/ owner, CodeContext/*!*/ context, IList<byte>/*!*/ buffer) {
_context = context;
_pythonBuffer = null;
_buffer = buffer;
_owner = owner;

Expand All @@ -695,11 +719,24 @@ internal PythonUnpackIterator(Struct/*!*/ owner, CodeContext/*!*/ context, IList
ValidateBufferLength();
}

internal PythonUnpackIterator(Struct/*!*/ owner, CodeContext/*!*/ context, IPythonBuffer/*!*/ buffer) {
_context = context;
_pythonBuffer = buffer;
_buffer = null;
_owner = owner;

_iter_current = null;
_next_offset = 0;
ValidateBufferLength();
}

private int BufferLength => _pythonBuffer?.NumBytes() ?? _buffer!.Count;

private void ValidateBufferLength() {
if (_owner.size == 0) {
throw Error(_context, "cannot iteratively unpack with a struct of length 0");
}
if (_buffer.Count % _owner.size != 0) {
if (BufferLength % _owner.size != 0) {
throw Error(_context, $"iterative unpacking requires a buffer of a multiple of {_owner.size} bytes");
}
}
Expand All @@ -716,30 +753,39 @@ private void ValidateBufferLength() {
#region IEnumerator<object> Members

[PythonHidden]
public object Current => _iter_current;
public object Current => _iter_current!;

[PythonHidden]
public bool MoveNext() {
if (_buffer.Count - _next_offset < _owner.size) {
if (BufferLength - _next_offset < _owner.size) {
return false;
}

_iter_current = _owner.unpack_from(_context, _buffer, _next_offset);
if (_pythonBuffer is null) {
_iter_current = _owner.unpack_from(_context, _buffer, _next_offset);
}
else {
_iter_current = _owner.unpack(_context, _pythonBuffer.AsReadOnlySpan().Slice(_next_offset, _owner.size).ToArray());
}
_next_offset += _owner.size;
return true;
}

void IEnumerator.Reset() => throw new NotSupportedException();

[PythonHidden]
public void Dispose() { }
public void Dispose() {
_pythonBuffer?.Dispose();
}

#endregion

public int __length_hint__()
=> (_buffer.Count - _next_offset) / _owner.size;
=> (BufferLength - _next_offset) / _owner.size;
}

#nullable restore

#endregion

#region Compiled Format
Expand Down Expand Up @@ -894,11 +940,21 @@ public static void pack_into(CodeContext/*!*/ context, object fmt, [NotNone] IBu
return GetStructFromCache(context, fmt).unpack_from(context, buffer, offset);
}

[Documentation("Unpack the buffer, containing packed C structure data, according to\nfmt, starting at offset. Requires len(buffer[offset:]) >= calcsize(fmt).")]
public static PythonTuple/*!*/ unpack_from(CodeContext/*!*/ context, object fmt, [NotNone] IBufferProtocol/*!*/ buffer, int offset = 0) {
return GetStructFromCache(context, fmt).unpack_from(context, buffer, offset);
}

[Documentation("Iteratively unpack the buffer, containing packed C structure data, according to\nfmt, starting at offset. Requires len(buffer[offset:]) >= calcsize(fmt).")]
public static PythonUnpackIterator/*!*/ iter_unpack(CodeContext/*!*/ context, object fmt, [BytesLike][NotNone] IList<byte>/*!*/ buffer) {
return GetStructFromCache(context, fmt).iter_unpack(context, buffer);
}

[Documentation("Iteratively unpack the buffer, containing packed C structure data, according to\nfmt, starting at offset. Requires len(buffer[offset:]) >= calcsize(fmt).")]
public static PythonUnpackIterator/*!*/ iter_unpack(CodeContext/*!*/ context, object fmt, [NotNone] IBufferProtocol/*!*/ buffer) {
return GetStructFromCache(context, fmt).iter_unpack(context, buffer);
}

#endregion

#region Write Helpers
Expand Down
14 changes: 11 additions & 3 deletions src/core/IronPython/Runtime/Binding/PythonOverloadResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ public override Candidate SelectBestConversionFor(DynamicMetaObject arg, Paramet
return basePreferred;
}

// Prefer the [BytesLike] overload when the argument type is assignable to it.
if (IsBytesLikeParameter(candidateTwo)) {
return candidateTwo.Type.IsAssignableFrom(arg.LimitType) ? Candidate.Two : Candidate.One;
}
if (IsBytesLikeParameter(candidateOne)) {
return candidateOne.Type.IsAssignableFrom(arg.LimitType) ? Candidate.One : Candidate.Two;
}

// Work around the choice made in Converter.PreferConvert
// This cannot be done using NarrowingLevel rules because it would confuse rules for selecting custom operators
if (level >= PythonNarrowing.IndexOperator && Converter.IsPythonBigInt(arg.LimitType)) {
Expand Down Expand Up @@ -141,15 +149,15 @@ public override bool CanConvertFrom(Type fromType, DynamicMetaObject fromArg, Pa
Type toType = toParameter.Type;

if (IsBytesLikeParameter(toParameter)) {

if ((fromType == typeof(PythonList) || fromType.IsSubclassOf(typeof(PythonList)))) {
if (fromType == typeof(PythonList) || fromType.IsSubclassOf(typeof(PythonList))) {
if (toType.IsGenericType &&
toType.GetGenericTypeDefinition() == typeof(IList<>)) {
return false;
}
}

if (typeof(IBufferProtocol).IsAssignableFrom(fromType)) {
// Apply this conversion only to real arguments, not parameter-type comparisons (in which case fromArg is null).
if (fromArg is not null && typeof(IBufferProtocol).IsAssignableFrom(fromType)) {
if (toParameter.Type == typeof(IList<byte>) || toParameter.Type == typeof(IReadOnlyList<byte>)) {
return true;
}
Expand Down
7 changes: 7 additions & 0 deletions tests/suite/test_struct.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ def test_unpack_from(self):
a, = struct.unpack_from(b'>H', b"\x00\x01")
self.assertEqual(a, 1)

data = b"\x00\x01\x00\x02"
a, = struct.unpack_from('>H', data, -2)
self.assertEqual(a, 2)

a, = struct.unpack_from('>H', memoryview(data), -2)
self.assertEqual(a, 2)

def test_pack_into(self):
# test string format string
result = array.array('b', [0, 0])
Expand Down