File Coverage

blib/lib/PlayStation/MemoryCard.pm
Criterion Covered Total %
statement 18 148 12.1
branch 0 44 0.0
condition 0 21 0.0
subroutine 6 21 28.5
pod 0 13 0.0
total 24 247 9.7


line stmt bran cond sub pod time code
1             package PlayStation::MemoryCard;
2             # Copyright (c) 2021 Gavin Hayes and others, see LICENSE in the root of the project
3 1     1   825 use version; our $VERSION = version->declare("v0.2.0");
  1         1564  
  1         5  
4 1     1   80 use strict;
  1         2  
  1         17  
5 1     1   4 use warnings;
  1         2  
  1         23  
6 1     1   482 use Encode qw(decode encode);
  1         8627  
  1         79  
7 1     1   8 use File::Basename;
  1         2  
  1         1452  
8              
9             sub parse_directory {
10 0     0 0   my ($directory) = @_;
11 0           my $inuse = unpack('C', $directory);
12 0           my $datasize = unpack('V', substr($directory, 0x4, 0x4));
13 0           my $linkindex = unpack('v', substr($directory, 0x8));
14 0           my $codestr = unpack('Z*', substr($directory, 0xA));
15 0           my @toxor = unpack('C127', $directory);
16 0           my $storedxor = unpack('C', substr($directory, 0x7F));
17              
18             #my @blocktypes = ('INUSE', 'SRTLINK', 'MIDLINK', 'ENDLINK', 'EMPTY', 'UNUSABLE', 'UNKNOWN');
19 0           my $blockcount = int($datasize / 0x2000);
20              
21 0           my $calcxor = 0;
22 0           foreach my $char (@toxor) {
23 0           $calcxor ^= $char;
24             }
25              
26             return {
27 0           'inuse' => $inuse,
28             'datasize' => $datasize,
29             'linkindex' => $linkindex,
30             'codename' => $codestr,
31             'xor' => $storedxor,
32              
33             'calcxor' => $calcxor,
34             'calcblocks' => $blockcount
35             };
36             }
37              
38             sub parse_file_header {
39 0     0 0   my ($file) = @_;
40 0           my $id = unpack('a2', $file);
41 0           my $displayflag = unpack('C', substr($file, 0x2));
42 0           my $blocknum = unpack('C', substr($file, 0x3));
43 0           my $shiftjisbuf = unpack('a64', substr($file, 0x4, 0x40));
44 0           my @clut = unpack('v16', substr($file, 0x60, 0x20));
45              
46              
47 0           my $iconfnt = 0;
48 0 0 0       if(($displayflag >= 0x11)|| ($displayflag <= 0x13)) {
49 0           $iconfnt = $displayflag - 0x10;
50             }
51              
52 0           my $firstnul = index($shiftjisbuf, "\0");
53 0 0         if($firstnul != -1) {
54 0           $shiftjisbuf = substr($shiftjisbuf, 0, $firstnul);
55             }
56 0           my $shiftjis = decode('shiftjis', $shiftjisbuf);
57              
58             return {
59 0           'id' => $id,
60             'displayflag' => $displayflag,
61             'blocknum' => $blocknum,
62             'titlebuf' => $shiftjisbuf,
63             'clut' => \@clut,
64             'title' => $shiftjis,
65             'framecnt' => $iconfnt
66             };
67             }
68              
69             sub is_mcd {
70 0     0 0   my ($res) = @_;
71              
72 0 0         (substr($res, 0, 2) eq 'MC') or return 0;
73              
74              
75             # A PSX memory card is 1 Mebibyte/ 128 kibibyte/ 131072 bytes
76             # 1 header block of 8192 and 15 data blocks of 8192.
77 0           return (length($res) == 131072);
78             }
79              
80             sub is_mcs {
81 0     0 0   my ($res) = @_;
82             # A PSX mcs save is 1 directory frame and X data frames
83 0           my $datasize = length($res) - 0x80;
84 0           return (($datasize % 0x2000) == 0);
85             }
86              
87             sub xordirectory {
88 0     0 0   my ($directory) = @_;
89 0           my @toxor = unpack('C127', $directory);
90 0           my $xor = 0;
91 0           foreach my $char (@toxor) {
92 0           $xor ^= $char;
93             }
94 0           return $xor;
95             }
96              
97              
98             sub load {
99 0     0 0   my ($class, $filename, $overridefilename) = @_;
100            
101 0           my $fh;
102 0 0         if($filename ne '-') {
103 0 0         open($fh, '<', $filename) or die("failed to open: $filename");
104             }
105             else {
106 0           $fh = *STDIN;
107 0           $filename = 'STDIN';
108             }
109 0           my %self = ('filename' => $filename, 'contents' => '');
110 0           my $res = read($fh, $self{'contents'}, 131073);
111              
112             # a mcd file (full memory card dump) should be the largest file
113 0 0 0       (($res) && ($res <= 131072)) or return undef;
114              
115 0 0 0       if(is_mcd($self{'contents'})) {
    0 0        
    0          
    0          
116 0           $self{'type'} = 'mcd';
117             }
118             elsif(is_mcs($self{'contents'})) {
119 0           $self{'type'} = 'mcs';
120             }
121             elsif(($res <= (15*0x2000)) && ($res >= 0x2000) && (($res % 0x2000) == 0)) {
122 0           $self{'type'} = 'rawsave';
123 0 0         my $filecodename = ($filename ne 'STDIN') ? basename($filename) : undef;
124 0 0         $self{'codename'} = $overridefilename ? $overridefilename : $filecodename;
125             }
126             elsif(substr($res, 0, 2) eq 'MC') {
127 0           warn("File starts with MC, but filesize is $res. Assuming type is mcd");
128 0           $self{'type'} = 'mcd';
129             }
130             else {
131 0           return undef;
132             }
133              
134 0 0 0       if(($self{'type'} eq 'mcd') || ($self{'type'} eq 'mcs')) {
135 0           $self{'hasdir'} = 1;
136             }
137              
138 0           bless \%self, $class;
139 0           return \%self;
140             }
141              
142             # loop through the directory entries of a MCD file, calling callback for each one
143             # if it's a startblock read in the save as pass it to the callback
144             sub foreachDirEntry {
145 0     0 0   my ($self, $callback) = @_;
146 0 0         ($self->{'type'} eq 'mcd') or die("Unhandled filetype");
147              
148 0           my $startindex = 1;
149 0           my $dataoffset = 0x2000;
150 0           my $maxcount = 15;
151            
152 0           for(my $i = $startindex; $i < ($startindex+$maxcount); $i++) {
153 0           my $entrydata = substr($self->{'contents'}, ($i * 0x80), 0x80);
154 0           my $entry = parse_directory($entrydata);
155 0           my $save;
156 0 0         if($entry->{'inuse'} == 0x51) {
157             $save = {
158             'filename' => $entry->{'codename'},
159 0           'data' => substr($self->{'contents'}, $dataoffset, $entry->{'datasize'}),
160             };
161             }
162 0           $callback->($entry, $save, $entrydata);
163 0           $dataoffset += 0x2000;
164             }
165             }
166              
167             sub _readMCSSave {
168 0     0     my ($self) = @_;
169 0           my $entrydata = substr($self->{'contents'}, 0, 0x80);
170 0           my $entry = parse_directory($entrydata);
171              
172             return {
173             'filename' => $entry->{'codename'},
174 0           'data' => substr($self->{'contents'}, 0x80, $entry->{'datasize'})
175             };
176             }
177              
178             sub _readRawSave {
179 0     0     my ($self) = @_;
180 0 0         $self->{'codename'} or die("cannot read raw save without a filename");
181             return {
182             'filename' => $self->{'codename'},
183 0           'data' => $self->{'contents'}
184             };
185             }
186              
187              
188             sub readSave {
189 0     0 0   my ($self) = @_;
190 0 0         if($self->{'type'} eq 'mcs') {
    0          
191 0           return _readMCSSave($self);
192             }
193             elsif($self->{'type'} eq 'rawsave') {
194 0           return _readRawSave($self);
195             }
196             else {
197 0           die("unimplemented type");
198             }
199             }
200              
201             sub FormatSaveFirstDirEntry {
202 0     0 0   my ($save, $dirindex) = @_;
203 0           my $savelen = length($save->{'data'});
204 0           my $blockcount = length($save->{'data'}) / 0x2000;
205 0 0         (($blockcount % 1) == 0) or die("not integer blocksize");
206 0 0         ($blockcount >= 1) or die("must have at least one block");
207 0 0         my $blockptr = ($blockcount == 1) ? 0xFFFF : $dirindex;
208 0           my $directory = pack('VVvZ21x96', 0x51, $savelen, $blockptr, $save->{'filename'});
209 0           $directory .= pack('C', xordirectory($directory));
210              
211 0           return ($directory, $blockcount);
212             }
213              
214             sub FormatSaveAsMCD {
215 0     0 0   my ($dirstart, $save) = @_;
216              
217 0           my $dirindex = ($dirstart / 0x80);
218 0           my ($directory, $blockcount) = FormatSaveFirstDirEntry($save, $dirindex);
219            
220             # format the possible mid and end link directories
221 0 0         if($blockcount > 1) {
222 0           while ($blockcount > 2) {
223 0           $dirindex++;
224 0           my $newdir = pack('VVvx117', 0x52, 0x0, $dirindex);
225 0           $newdir .= pack('C', xordirectory($newdir));
226 0           $directory .= $newdir;
227 0           $blockcount--;
228             }
229 0           my $newdir = pack('VVvx117', 0x53, 0x0, 0xFFFF);
230 0           $newdir .= pack('C', xordirectory($newdir));
231 0           $directory .= $newdir;
232             }
233            
234             return {
235             'dirdata' => $directory,
236 0           'savedata' => $save->{'data'}
237             };
238             }
239              
240             sub FormatSaveAsMCS {
241 0     0 0   my ($save) = @_;
242 0           my ($directory, $blockcount) = FormatSaveFirstDirEntry($save, 1);
243 0           return $directory .= $save->{'data'};
244             }
245              
246             sub SaveNameAndTitleMatch {
247 0     0 0   my ($save, $string) = @_;
248 0   0       $save->{'header'} //= PlayStation::MemoryCard::parse_file_header($save->{'data'});
249 0           my $title = $save->{'header'}{'title'};
250 0           my $asciititle = $title;
251 1     1   505 $asciititle =~ tr/\x{3000}\x{FF01}-\x{FF5E}/ -~/; # fullwidth to half-width if possible
  1         13  
  1         13  
  0            
252             #warn("title: $title");
253             #warn("searchfname: $string");
254             #warn("asciititle: $asciititle");
255             return (!$string ||
256             ($save->{'filename'} eq $string) || ($title eq $string) || ($asciititle eq $string) ||
257 0   0       ($save->{'filename'} =~ /\Q$string\E/i) || ($title =~ /\Q$string\E/i) || ($asciititle =~ /\Q$string\E/i));
258             }
259              
260             sub BlankMCD {
261 0     0 0   my $cardbuf = pack('x131072');
262              
263             # header frame
264 0           substr($cardbuf, 0, 2, 'MC');
265 0           substr($cardbuf, 0x7F, 1, pack('C', 0x0E));
266             # directory frames
267 0           for(my $i = 1; $i < 16; $i++) {
268 0           my $frameoffset = $i*0x80;
269 0           substr($cardbuf, $frameoffset, 1, pack('C', 0xA0));
270 0           substr($cardbuf, $frameoffset+0x8, 2, pack('v', 0xFFFF));
271 0           substr($cardbuf, $frameoffset+0x7F, 1, pack('C', 0xA0));
272             }
273             # broken sector list
274 0           for(my $i = 16; $i < 36; $i++) {
275 0           my $frameoffset = $i*0x80;
276 0           substr($cardbuf, $frameoffset, 4, pack('V', 0xFFFFFFFF));
277 0           substr($cardbuf, $frameoffset+0x8, 2, pack('v', 0xFFFF));
278             }
279             # broken sector replacement data 36-55
280             # unused frames 56-62
281             # write test frame 63
282             # file blocks
283              
284 0           return $cardbuf;
285             }
286              
287             1;
288              
289             __END__