File Coverage

lib/Net/BitTorrent/Storage/File.pm
Criterion Covered Total %
statement 20 20 100.0
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 27 27 100.0


line stmt bran cond sub pod time code
1 21     21   271 use v5.40;
  21         74  
2 21     21   125 use feature 'class';
  21         62  
  21         2970  
3 21     21   149 no warnings 'experimental::class';
  21         51  
  21         1088  
4 21     21   168 use Net::BitTorrent::Emitter;
  21         41  
  21         2225  
5             class Net::BitTorrent::Storage::File v2.0.0 : isa(Net::BitTorrent::Emitter) {
6 21     21   11170 use Digest::Merkle::SHA256;
  21         148999  
  21         1033  
7 21     21   20754 use Path::Tiny qw();
  21         349972  
  21         1005  
8 21     21   198 use Digest::SHA qw[sha256];
  21         147  
  21         53896  
9             field $file_path : param(path) : reader(path);
10             field $size : param : reader;
11             field $pieces_root : param : reader = undef;
12             field $piece_size : param : reader = 0;
13             field $merkle : reader;
14             ADJUST {
15             $file_path = Path::Tiny::path($file_path);
16             if ($pieces_root) {
17             $merkle = Digest::Merkle::SHA256->new( file_size => $size );
18             }
19             }
20              
21             method verify_block ( $index, $data ) {
22             if ( !$merkle ) {
23             $self->_emit( log => 'File does not have Merkle tree (no pieces root)', level => 'fatal' );
24             return 0;
25             }
26             my $old_hash = $merkle->get_node( $merkle->height, $index );
27             my $hash = sha256($data);
28             $merkle->set_block( $index, $hash );
29             if ( $merkle->root eq $pieces_root ) {
30             return 1;
31             }
32             else {
33             $merkle->set_block( $index, $old_hash );
34             return 0;
35             }
36             }
37              
38             method verify_block_audit ( $index, $data, $audit_path ) {
39             if ( !$pieces_root ) {
40             $self->_emit( log => 'File does not have pieces root', level => 'fatal' );
41             return 0;
42             }
43             return Digest::Merkle::SHA256->verify_hash( $index, sha256($data), $audit_path, $pieces_root );
44             }
45              
46             method verify_piece_v2 ( $index, $data, $expected_hash ) {
47              
48             # In BT v2, piece layer hashes are nodes at a specific level in the file's merkle tree.
49             # If the piece size == block size (16KiB), the hash is just sha256(data).
50             # Otherwise, it's the root of a mini-tree of the blocks in that piece.
51             my $block_size = $merkle ? $merkle->block_size : 16384;
52             my $blocks_per_piece = int( $piece_size / $block_size );
53             my $num_blocks = int( ( length($data) + $block_size - 1 ) / $block_size );
54             my $actual_hash;
55             if ( $num_blocks == 1 ) {
56             $actual_hash = sha256($data);
57             }
58             else {
59             my $tmp_merkle = Digest::Merkle::SHA256->new( file_size => length($data), block_size => $block_size );
60             for ( my $i = 0; $i < $num_blocks; $i++ ) {
61             $tmp_merkle->set_block( $i, sha256( substr( $data, $i * $block_size, $block_size ) ) );
62             }
63             $actual_hash = $tmp_merkle->root;
64             }
65             if ( $actual_hash eq $expected_hash ) {
66              
67             # If we have a full merkle tree, we can populate its leaves now
68             if ($merkle) {
69             for ( my $i = 0; $i < $num_blocks; $i++ ) {
70             $merkle->set_block( $index * $blocks_per_piece + $i, sha256( substr( $data, $i * $block_size, $block_size ) ) );
71             }
72             }
73             return 1;
74             }
75             return 0;
76             }
77              
78             method read ( $offset, $length ) {
79             return '' if $length <= 0;
80             return undef unless $file_path->exists;
81             my $fh = $file_path->openr_raw;
82             seek $fh, $offset, 0;
83             read( $fh, my $chunk, $length );
84             return $chunk;
85             }
86              
87             method write ( $offset, $data ) {
88             $self->_ensure_exists();
89             $self->_emit( log => " [DEBUG] Writing " . length($data) . " bytes to $file_path at offset $offset\n", level => 'debug' );
90             my $fh = $file_path->openrw_raw;
91             seek $fh, $offset, 0;
92             print {$fh} $data or do {
93             $self->_emit( log => "Failed to write to $file_path: $!", level => 'fatal' );
94             return;
95             };
96             $fh->flush();
97             }
98              
99             method _ensure_exists () {
100             return if $file_path->exists;
101             $file_path->parent->mkpath;
102             if ( $^O eq 'MSWin32' ) {
103             try {
104             require Win32::File;
105              
106             # Create empty file first
107             $file_path->touch;
108              
109             # Set sparse attribute
110             Win32::File::SetAttributes( $file_path->stringify, Win32::File::SPARSE_FILE() );
111              
112             # Set size by seeking and writing one byte at the end
113             if ( $size > 0 ) {
114             my $fh = $file_path->openw_raw;
115             seek $fh, $size - 1, 0;
116             print {$fh} "\0";
117             close $fh;
118             }
119             }
120             catch ($e) {
121              
122             # Fallback to simple allocation if Win32::File fails or is missing
123             $self->_simple_allocate();
124             }
125             }
126             else {
127             # Unix-like: truncate is usually enough for sparse files on modern FS (ext4, apfs, etc)
128             try {
129             if ( $size > 0 ) {
130             truncate( $file_path->stringify, $size ) or $self->_simple_allocate();
131             }
132             else {
133             $file_path->touch;
134             }
135             }
136             catch ($e) {
137             $self->_simple_allocate();
138             }
139             }
140             }
141              
142             method _simple_allocate () {
143             if ( $size > 0 ) {
144             my $fh = $file_path->openw_raw;
145             seek $fh, $size - 1, 0;
146             print {$fh} "\0";
147             close $fh;
148             }
149             else {
150             $file_path->touch;
151             }
152             }
153              
154             method dump_state () {
155             return { merkle => ( $merkle ? $merkle->dump_state : undef ), };
156             }
157              
158             method load_state ($state) {
159             if ( $merkle && $state->{merkle} ) {
160             $merkle->load_state( $state->{merkle} );
161             }
162             }
163             } 1;