mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 07:45:30 +00:00
111 lines
3.6 KiB
Dart
111 lines
3.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
enum TaskPieceState { pending, completed }
|
|
|
|
/// Decoded view of Gopeed's ordered `bitset-v1` piece map.
|
|
///
|
|
/// A set bit means the piece is complete and verified. The decoder keeps the
|
|
/// compact bytes instead of expanding large torrents into per-piece objects;
|
|
/// [stateAt] reads any piece in O(1).
|
|
class TaskPieceMap {
|
|
const TaskPieceMap._({
|
|
required this.encoding,
|
|
required this.pieceCount,
|
|
required this.pieceSize,
|
|
required this.completedPieces,
|
|
required Uint8List data,
|
|
}) : _data = data;
|
|
|
|
static const supportedEncoding = 'bitset-v1';
|
|
|
|
final String encoding;
|
|
final int pieceCount;
|
|
final int pieceSize;
|
|
final int completedPieces;
|
|
final Uint8List _data;
|
|
|
|
static TaskPieceMap empty() =>
|
|
TaskPieceMap._(encoding: supportedEncoding, pieceCount: 0, pieceSize: 0, completedPieces: 0, data: Uint8List(0));
|
|
|
|
/// Decodes the Base64 bitset returned by Go. Piece N uses bit `N % 8` of
|
|
/// byte `N ~/ 8`, from least-significant to most-significant bit.
|
|
static TaskPieceMap? fromJson(Object? value) {
|
|
if (value is! Map) return null;
|
|
final json = {for (final entry in value.entries) entry.key.toString(): entry.value};
|
|
final encoding = json['encoding'];
|
|
final pieceCount = json['pieceCount'];
|
|
final pieceSize = json['pieceSize'];
|
|
final completedPieces = json['completedPieces'];
|
|
final encodedData = json['data'];
|
|
if (encoding != supportedEncoding ||
|
|
pieceCount is! int ||
|
|
pieceCount < 0 ||
|
|
pieceSize is! int ||
|
|
pieceSize < 0 ||
|
|
completedPieces is! int ||
|
|
completedPieces < 0 ||
|
|
completedPieces > pieceCount ||
|
|
encodedData is! String) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
final data = base64Decode(encodedData);
|
|
final expectedLength = (pieceCount + 7) ~/ 8;
|
|
if (data.length != expectedLength) return null;
|
|
|
|
var actualCompleted = 0;
|
|
for (var index = 0; index < pieceCount; index++) {
|
|
if ((data[index ~/ 8] & (1 << (index % 8))) != 0) actualCompleted++;
|
|
}
|
|
if (actualCompleted != completedPieces) return null;
|
|
|
|
return TaskPieceMap._(
|
|
encoding: encoding,
|
|
pieceCount: pieceCount,
|
|
pieceSize: pieceSize,
|
|
completedPieces: completedPieces,
|
|
data: data,
|
|
);
|
|
} on FormatException {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Packs local states with the same bit order. Used for widget construction
|
|
/// and tests; API responses are decoded through [fromJson].
|
|
factory TaskPieceMap.fromStates(List<TaskPieceState> states, {int pieceSize = 0}) {
|
|
final data = Uint8List((states.length + 7) ~/ 8);
|
|
var completedPieces = 0;
|
|
for (var index = 0; index < states.length; index++) {
|
|
if (states[index] != TaskPieceState.completed) continue;
|
|
data[index ~/ 8] |= 1 << (index % 8);
|
|
completedPieces++;
|
|
}
|
|
return TaskPieceMap._(
|
|
encoding: supportedEncoding,
|
|
pieceCount: states.length,
|
|
pieceSize: pieceSize,
|
|
completedPieces: completedPieces,
|
|
data: data,
|
|
);
|
|
}
|
|
|
|
bool get isEmpty => pieceCount == 0;
|
|
bool get isNotEmpty => !isEmpty;
|
|
|
|
TaskPieceState stateAt(int index) {
|
|
RangeError.checkValidIndex(index, this, 'index', pieceCount);
|
|
final completed = (_data[index ~/ 8] & (1 << (index % 8))) != 0;
|
|
return completed ? TaskPieceState.completed : TaskPieceState.pending;
|
|
}
|
|
|
|
bool contains(TaskPieceState state) => switch (state) {
|
|
TaskPieceState.completed => completedPieces > 0,
|
|
TaskPieceState.pending => completedPieces < pieceCount,
|
|
};
|
|
|
|
List<TaskPieceState> toStates() => List.generate(pieceCount, stateAt, growable: false);
|
|
}
|