451ab91e |
1 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * |
2 | * Mupen64plus - rom.c * |
3 | * Mupen64Plus homepage: http://code.google.com/p/mupen64plus/ * |
4 | * Copyright (C) 2008 Tillin9 * |
5 | * Copyright (C) 2002 Hacktarux * |
6 | * * |
7 | * This program is free software; you can redistribute it and/or modify * |
8 | * it under the terms of the GNU General Public License as published by * |
9 | * the Free Software Foundation; either version 2 of the License, or * |
10 | * (at your option) any later version. * |
11 | * * |
12 | * This program is distributed in the hope that it will be useful, * |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
15 | * GNU General Public License for more details. * |
16 | * * |
17 | * You should have received a copy of the GNU General Public License * |
18 | * along with this program; if not, write to the * |
19 | * Free Software Foundation, Inc., * |
20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * |
21 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ |
22 | |
23 | #include <stdio.h> |
24 | #include <stdlib.h> |
25 | #include <string.h> |
26 | #include <ctype.h> |
27 | |
28 | #define M64P_CORE_PROTOTYPES 1 |
29 | #include "api/m64p_types.h" |
30 | #include "api/callbacks.h" |
31 | #include "api/config.h" |
32 | #include "api/m64p_config.h" |
33 | |
34 | #include "md5.h" |
35 | #include "rom.h" |
36 | #include "main.h" |
37 | #include "util.h" |
38 | |
39 | #include "memory/memory.h" |
40 | #include "osal/preproc.h" |
41 | #include "osd/osd.h" |
42 | |
43 | #define DEFAULT 16 |
44 | |
45 | #define CHUNKSIZE 1024*128 /* Read files 128KB at a time. */ |
46 | |
47 | static romdatabase_entry* ini_search_by_md5(md5_byte_t* md5); |
48 | |
49 | static _romdatabase g_romdatabase; |
50 | |
51 | /* Global loaded rom memory space. */ |
52 | unsigned char* rom = NULL; |
53 | /* Global loaded rom size. */ |
54 | int rom_size = 0; |
55 | |
56 | unsigned char isGoldeneyeRom = 0; |
57 | |
2d262872 |
58 | extern int count_per_op; |
59 | |
451ab91e |
60 | m64p_rom_header ROM_HEADER; |
61 | rom_params ROM_PARAMS; |
62 | m64p_rom_settings ROM_SETTINGS; |
63 | |
64 | static m64p_system_type rom_country_code_to_system_type(unsigned short country_code); |
65 | static int rom_system_type_to_ai_dac_rate(m64p_system_type system_type); |
66 | static int rom_system_type_to_vi_limit(m64p_system_type system_type); |
67 | |
68 | /* Tests if a file is a valid N64 rom by checking the first 4 bytes. */ |
69 | static int is_valid_rom(const unsigned char *buffer) |
70 | { |
71 | /* Test if rom is a native .z64 image with header 0x80371240. [ABCD] */ |
72 | if((buffer[0]==0x80)&&(buffer[1]==0x37)&&(buffer[2]==0x12)&&(buffer[3]==0x40)) |
73 | return 1; |
74 | /* Test if rom is a byteswapped .v64 image with header 0x37804012. [BADC] */ |
75 | else if((buffer[0]==0x37)&&(buffer[1]==0x80)&&(buffer[2]==0x40)&&(buffer[3]==0x12)) |
76 | return 1; |
77 | /* Test if rom is a wordswapped .n64 image with header 0x40123780. [DCBA] */ |
78 | else if((buffer[0]==0x40)&&(buffer[1]==0x12)&&(buffer[2]==0x37)&&(buffer[3]==0x80)) |
79 | return 1; |
80 | else |
81 | return 0; |
82 | } |
83 | |
84 | /* If rom is a .v64 or .n64 image, byteswap or wordswap loadlength amount of |
85 | * rom data to native .z64 before forwarding. Makes sure that data extraction |
86 | * and MD5ing routines always deal with a .z64 image. |
87 | */ |
88 | static void swap_rom(unsigned char* localrom, unsigned char* imagetype, int loadlength) |
89 | { |
90 | unsigned char temp; |
91 | int i; |
92 | |
93 | /* Btyeswap if .v64 image. */ |
94 | if(localrom[0]==0x37) |
95 | { |
96 | *imagetype = V64IMAGE; |
97 | for (i = 0; i < loadlength; i+=2) |
98 | { |
99 | temp=localrom[i]; |
100 | localrom[i]=localrom[i+1]; |
101 | localrom[i+1]=temp; |
102 | } |
103 | } |
104 | /* Wordswap if .n64 image. */ |
105 | else if(localrom[0]==0x40) |
106 | { |
107 | *imagetype = N64IMAGE; |
108 | for (i = 0; i < loadlength; i+=4) |
109 | { |
110 | temp=localrom[i]; |
111 | localrom[i]=localrom[i+3]; |
112 | localrom[i+3]=temp; |
113 | temp=localrom[i+1]; |
114 | localrom[i+1]=localrom[i+2]; |
115 | localrom[i+2]=temp; |
116 | } |
117 | } |
118 | else |
119 | *imagetype = Z64IMAGE; |
120 | } |
121 | |
122 | m64p_error open_rom(const unsigned char* romimage, unsigned int size) |
123 | { |
124 | md5_state_t state; |
125 | md5_byte_t digest[16]; |
126 | romdatabase_entry* entry; |
127 | char buffer[256]; |
128 | unsigned char imagetype; |
129 | int i; |
130 | |
131 | /* check input requirements */ |
132 | if (rom != NULL) |
133 | { |
134 | DebugMessage(M64MSG_ERROR, "open_rom(): previous ROM image was not freed"); |
135 | return M64ERR_INTERNAL; |
136 | } |
137 | if (romimage == NULL || !is_valid_rom(romimage)) |
138 | { |
139 | DebugMessage(M64MSG_ERROR, "open_rom(): not a valid ROM image"); |
140 | return M64ERR_INPUT_INVALID; |
141 | } |
142 | |
143 | /* Clear Byte-swapped flag, since ROM is now deleted. */ |
144 | g_MemHasBeenBSwapped = 0; |
145 | /* allocate new buffer for ROM and copy into this buffer */ |
146 | rom_size = size; |
147 | rom = (unsigned char *) malloc(size); |
148 | if (rom == NULL) |
149 | return M64ERR_NO_MEMORY; |
150 | memcpy(rom, romimage, size); |
151 | swap_rom(rom, &imagetype, rom_size); |
152 | |
153 | memcpy(&ROM_HEADER, rom, sizeof(m64p_rom_header)); |
154 | |
155 | /* Calculate MD5 hash */ |
156 | md5_init(&state); |
157 | md5_append(&state, (const md5_byte_t*)rom, rom_size); |
158 | md5_finish(&state, digest); |
159 | for ( i = 0; i < 16; ++i ) |
160 | sprintf(buffer+i*2, "%02X", digest[i]); |
161 | buffer[32] = '\0'; |
162 | strcpy(ROM_SETTINGS.MD5, buffer); |
163 | |
164 | /* add some useful properties to ROM_PARAMS */ |
165 | ROM_PARAMS.systemtype = rom_country_code_to_system_type(ROM_HEADER.Country_code); |
166 | ROM_PARAMS.vilimit = rom_system_type_to_vi_limit(ROM_PARAMS.systemtype); |
167 | ROM_PARAMS.aidacrate = rom_system_type_to_ai_dac_rate(ROM_PARAMS.systemtype); |
168 | |
169 | memcpy(ROM_PARAMS.headername, ROM_HEADER.Name, 20); |
170 | ROM_PARAMS.headername[20] = '\0'; |
171 | trim(ROM_PARAMS.headername); /* Remove trailing whitespace from ROM name. */ |
172 | |
173 | /* Look up this ROM in the .ini file and fill in goodname, etc */ |
174 | if ((entry=ini_search_by_md5(digest)) != NULL || |
175 | (entry=ini_search_by_crc(sl(ROM_HEADER.CRC1),sl(ROM_HEADER.CRC2))) != NULL) |
176 | { |
177 | strncpy(ROM_SETTINGS.goodname, entry->goodname, 255); |
178 | ROM_SETTINGS.goodname[255] = '\0'; |
179 | ROM_SETTINGS.savetype = entry->savetype; |
180 | ROM_SETTINGS.status = entry->status; |
181 | ROM_SETTINGS.players = entry->players; |
182 | ROM_SETTINGS.rumble = entry->rumble; |
2d262872 |
183 | delay_si = entry->delay_si; |
184 | count_per_op = entry->count_per_op; |
451ab91e |
185 | } |
186 | else |
187 | { |
188 | strcpy(ROM_SETTINGS.goodname, ROM_PARAMS.headername); |
189 | strcat(ROM_SETTINGS.goodname, " (unknown rom)"); |
190 | ROM_SETTINGS.savetype = NONE; |
191 | ROM_SETTINGS.status = 0; |
192 | ROM_SETTINGS.players = 0; |
193 | ROM_SETTINGS.rumble = 0; |
2d262872 |
194 | delay_si = -1; |
195 | count_per_op = -1; |
196 | } |
451ab91e |
197 | |
198 | /* print out a bunch of info about the ROM */ |
199 | DebugMessage(M64MSG_INFO, "Goodname: %s", ROM_SETTINGS.goodname); |
200 | DebugMessage(M64MSG_INFO, "Name: %s", ROM_HEADER.Name); |
201 | imagestring(imagetype, buffer); |
202 | DebugMessage(M64MSG_INFO, "MD5: %s", ROM_SETTINGS.MD5); |
203 | DebugMessage(M64MSG_INFO, "CRC: %x %x", sl(ROM_HEADER.CRC1), sl(ROM_HEADER.CRC2)); |
204 | DebugMessage(M64MSG_INFO, "Imagetype: %s", buffer); |
205 | DebugMessage(M64MSG_INFO, "Rom size: %d bytes (or %d Mb or %d Megabits)", rom_size, rom_size/1024/1024, rom_size/1024/1024*8); |
206 | DebugMessage(M64MSG_VERBOSE, "ClockRate = %x", sl(ROM_HEADER.ClockRate)); |
207 | DebugMessage(M64MSG_INFO, "Version: %x", sl(ROM_HEADER.Release)); |
208 | if(sl(ROM_HEADER.Manufacturer_ID) == 'N') |
209 | DebugMessage(M64MSG_INFO, "Manufacturer: Nintendo"); |
210 | else |
211 | DebugMessage(M64MSG_INFO, "Manufacturer: %x", sl(ROM_HEADER.Manufacturer_ID)); |
212 | DebugMessage(M64MSG_VERBOSE, "Cartridge_ID: %x", ROM_HEADER.Cartridge_ID); |
213 | countrycodestring(ROM_HEADER.Country_code, buffer); |
214 | DebugMessage(M64MSG_INFO, "Country: %s", buffer); |
215 | DebugMessage(M64MSG_VERBOSE, "PC = %x", sl((unsigned int)ROM_HEADER.PC)); |
216 | DebugMessage(M64MSG_VERBOSE, "Save type: %d", ROM_SETTINGS.savetype); |
2d262872 |
217 | if (delay_si>=0) DebugMessage(M64MSG_INFO, "Delay SI: %d", delay_si); |
218 | if (count_per_op>=0) DebugMessage(M64MSG_INFO, "Count Per OP: %d", count_per_op); |
451ab91e |
219 | |
220 | //Prepare Hack for GOLDENEYE |
221 | isGoldeneyeRom = 0; |
222 | if(strcmp(ROM_PARAMS.headername, "GOLDENEYE") == 0) |
223 | isGoldeneyeRom = 1; |
224 | |
225 | return M64ERR_SUCCESS; |
226 | } |
227 | |
228 | m64p_error close_rom(void) |
229 | { |
230 | if (rom == NULL) |
231 | return M64ERR_INVALID_STATE; |
232 | |
233 | free(rom); |
234 | rom = NULL; |
235 | |
236 | /* Clear Byte-swapped flag, since ROM is now deleted. */ |
237 | g_MemHasBeenBSwapped = 0; |
238 | DebugMessage(M64MSG_STATUS, "Rom closed."); |
239 | |
240 | return M64ERR_SUCCESS; |
241 | } |
242 | |
243 | /********************************************************************************************/ |
244 | /* ROM utility functions */ |
245 | |
246 | // Get the system type associated to a ROM country code. |
247 | static m64p_system_type rom_country_code_to_system_type(unsigned short country_code) |
248 | { |
249 | switch (country_code & 0xFF) |
250 | { |
251 | // PAL codes |
252 | case 0x44: |
253 | case 0x46: |
254 | case 0x49: |
255 | case 0x50: |
256 | case 0x53: |
257 | case 0x55: |
258 | case 0x58: |
259 | case 0x59: |
260 | return SYSTEM_PAL; |
261 | |
262 | // NTSC codes |
263 | case 0x37: |
264 | case 0x41: |
265 | case 0x45: |
266 | case 0x4a: |
267 | default: // Fallback for unknown codes |
268 | return SYSTEM_NTSC; |
269 | } |
270 | } |
271 | |
272 | // Get the VI (vertical interrupt) limit associated to a ROM system type. |
273 | static int rom_system_type_to_vi_limit(m64p_system_type system_type) |
274 | { |
275 | switch (system_type) |
276 | { |
277 | case SYSTEM_PAL: |
278 | case SYSTEM_MPAL: |
279 | return 50; |
280 | |
281 | case SYSTEM_NTSC: |
282 | default: |
283 | return 60; |
284 | } |
285 | } |
286 | |
287 | static int rom_system_type_to_ai_dac_rate(m64p_system_type system_type) |
288 | { |
289 | switch (system_type) |
290 | { |
291 | case SYSTEM_PAL: |
292 | return 49656530; |
293 | case SYSTEM_MPAL: |
294 | return 48628316; |
295 | case SYSTEM_NTSC: |
296 | default: |
297 | return 48681812; |
298 | } |
299 | } |
300 | |
301 | /********************************************************************************************/ |
302 | /* INI Rom database functions */ |
303 | |
304 | void romdatabase_open(void) |
305 | { |
306 | FILE *fPtr; |
307 | char buffer[256]; |
308 | romdatabase_search* search = NULL; |
309 | romdatabase_search** next_search; |
310 | |
311 | int counter, value, lineno; |
312 | unsigned char index; |
313 | const char *pathname = ConfigGetSharedDataFilepath("mupen64plus.ini"); |
314 | |
315 | if(g_romdatabase.have_database) |
316 | return; |
317 | |
318 | /* Open romdatabase. */ |
319 | if (pathname == NULL || (fPtr = fopen(pathname, "rb")) == NULL) |
320 | { |
321 | DebugMessage(M64MSG_ERROR, "Unable to open rom database file '%s'.", pathname); |
322 | return; |
323 | } |
324 | |
325 | g_romdatabase.have_database = 1; |
326 | |
327 | /* Clear premade indices. */ |
328 | for(counter = 0; counter < 255; ++counter) |
329 | g_romdatabase.crc_lists[counter] = NULL; |
330 | for(counter = 0; counter < 255; ++counter) |
331 | g_romdatabase.md5_lists[counter] = NULL; |
332 | g_romdatabase.list = NULL; |
333 | |
334 | next_search = &g_romdatabase.list; |
335 | |
336 | /* Parse ROM database file */ |
337 | for (lineno = 1; fgets(buffer, 255, fPtr) != NULL; lineno++) |
338 | { |
339 | char *line = buffer; |
340 | ini_line l = ini_parse_line(&line); |
341 | switch (l.type) |
342 | { |
343 | case INI_SECTION: |
344 | { |
345 | md5_byte_t md5[16]; |
346 | if (!parse_hex(l.name, md5, 16)) |
347 | { |
348 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid MD5 on line %i", lineno); |
349 | search = NULL; |
350 | continue; |
351 | } |
352 | |
353 | *next_search = (romdatabase_search*)malloc(sizeof(romdatabase_search)); |
354 | search = *next_search; |
355 | next_search = &search->next_entry; |
356 | |
357 | search->entry.goodname = NULL; |
358 | memcpy(search->entry.md5, md5, 16); |
359 | search->entry.refmd5 = NULL; |
360 | search->entry.crc1 = 0; |
361 | search->entry.crc2 = 0; |
362 | search->entry.status = 0; /* Set default to 0 stars. */ |
363 | search->entry.savetype = DEFAULT; |
364 | search->entry.players = DEFAULT; |
365 | search->entry.rumble = DEFAULT; |
2d262872 |
366 | /*SEB*/ |
367 | search->entry.delay_si=-1; |
368 | search->entry.count_per_op=-1; |
451ab91e |
369 | |
370 | search->next_entry = NULL; |
371 | search->next_crc = NULL; |
372 | /* Index MD5s by first 8 bits. */ |
373 | index = search->entry.md5[0]; |
374 | search->next_md5 = g_romdatabase.md5_lists[index]; |
375 | g_romdatabase.md5_lists[index] = search; |
376 | |
377 | break; |
378 | } |
379 | case INI_PROPERTY: |
380 | // This happens if there's stray properties before any section, |
381 | // or if some error happened on INI_SECTION (e.g. parsing). |
382 | if (search == NULL) |
383 | { |
384 | DebugMessage(M64MSG_WARNING, "ROM Database: Ignoring property on line %i", lineno); |
385 | continue; |
386 | } |
387 | if(!strcmp(l.name, "GoodName")) |
388 | { |
389 | search->entry.goodname = strdup(l.value); |
390 | } |
391 | else if(!strcmp(l.name, "CRC")) |
392 | { |
393 | char garbage_sweeper; |
394 | if (sscanf(l.value, "%X %X%c", &search->entry.crc1, |
395 | &search->entry.crc2, &garbage_sweeper) == 2) |
396 | { |
397 | /* Index CRCs by first 8 bits. */ |
398 | index = search->entry.crc1 >> 24; |
399 | search->next_crc = g_romdatabase.crc_lists[index]; |
400 | g_romdatabase.crc_lists[index] = search; |
401 | } |
402 | else |
403 | { |
404 | search->entry.crc1 = search->entry.crc2 = 0; |
405 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid CRC on line %i", lineno); |
406 | } |
407 | } |
408 | else if(!strcmp(l.name, "RefMD5")) |
409 | { |
410 | md5_byte_t md5[16]; |
411 | if (parse_hex(l.value, md5, 16)) |
412 | { |
413 | search->entry.refmd5 = (md5_byte_t*)malloc(16*sizeof(md5_byte_t)); |
414 | memcpy(search->entry.refmd5, md5, 16); |
415 | } |
416 | else |
417 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid RefMD5 on line %i", lineno); |
418 | } |
419 | else if(!strcmp(l.name, "SaveType")) |
420 | { |
421 | if(!strcmp(l.value, "Eeprom 4KB")) |
422 | search->entry.savetype = EEPROM_4KB; |
423 | else if(!strcmp(l.value, "Eeprom 16KB")) |
424 | search->entry.savetype = EEPROM_16KB; |
425 | else if(!strcmp(l.value, "SRAM")) |
426 | search->entry.savetype = SRAM; |
427 | else if(!strcmp(l.value, "Flash RAM")) |
428 | search->entry.savetype = FLASH_RAM; |
429 | else if(!strcmp(l.value, "Controller Pack")) |
430 | search->entry.savetype = CONTROLLER_PACK; |
431 | else if(!strcmp(l.value, "None")) |
432 | search->entry.savetype = NONE; |
433 | else |
434 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid save type on line %i", lineno); |
435 | } |
436 | else if(!strcmp(l.name, "Status")) |
437 | { |
438 | if (string_to_int(l.value, &value) && value >= 0 && value < 6) |
439 | search->entry.status = value; |
440 | else |
441 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid status on line %i", lineno); |
442 | } |
443 | else if(!strcmp(l.name, "Players")) |
444 | { |
445 | if (string_to_int(l.value, &value) && value >= 0 && value < 8) |
446 | search->entry.players = value; |
447 | else |
448 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid player count on line %i", lineno); |
449 | } |
450 | else if(!strcmp(l.name, "Rumble")) |
451 | { |
452 | if(!strcmp(l.value, "Yes")) |
453 | search->entry.rumble = 1; |
454 | else if(!strcmp(l.value, "No")) |
455 | search->entry.rumble = 0; |
456 | else |
457 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid rumble string on line %i", lineno); |
458 | } |
2d262872 |
459 | else if(!strcmp(l.name, "DelaySI")) |
460 | { |
461 | if(!strcmp(l.value, "True")) |
462 | search->entry.delay_si = 1; |
463 | else if(!strcmp(l.value, "False")) |
464 | search->entry.delay_si = 0; |
465 | else |
466 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid DelaySI string on line %i", lineno); |
467 | } |
468 | else if(!strcmp(l.name, "CountPerOp")) |
469 | { |
470 | if (string_to_int(l.value, &value) && value >= 0 && value < 8) |
471 | search->entry.count_per_op = value; |
472 | else |
473 | DebugMessage(M64MSG_WARNING, "ROM Database: Invalid CountPerOp on line %i", lineno); |
474 | } |
451ab91e |
475 | else |
476 | { |
477 | DebugMessage(M64MSG_WARNING, "ROM Database: Unknown property on line %i", lineno); |
478 | } |
479 | break; |
480 | default: |
481 | break; |
482 | } |
483 | } |
484 | |
485 | fclose(fPtr); |
486 | |
487 | /* Resolve RefMD5 references */ |
488 | for (search = g_romdatabase.list; search != NULL; search = search->next_entry) |
489 | { |
490 | if (search->entry.refmd5 != NULL) |
491 | { |
492 | romdatabase_entry *ref = ini_search_by_md5(search->entry.refmd5); |
493 | if (ref != NULL) |
494 | { |
495 | if(ref->savetype!=DEFAULT) |
496 | search->entry.savetype = ref->savetype; |
497 | if(ref->status!=0) |
498 | search->entry.status = ref->status; |
499 | if(ref->players!=DEFAULT) |
500 | search->entry.players = ref->players; |
501 | if(ref->rumble!=DEFAULT) |
502 | search->entry.rumble = ref->rumble; |
503 | } |
504 | else |
505 | DebugMessage(M64MSG_WARNING, "ROM Database: Error solving RefMD5s"); |
506 | } |
507 | } |
508 | } |
509 | |
510 | void romdatabase_close(void) |
511 | { |
512 | if (!g_romdatabase.have_database) |
513 | return; |
514 | |
515 | while (g_romdatabase.list != NULL) |
516 | { |
517 | romdatabase_search* search = g_romdatabase.list->next_entry; |
518 | if(g_romdatabase.list->entry.goodname) |
519 | free(g_romdatabase.list->entry.goodname); |
520 | if(g_romdatabase.list->entry.refmd5) |
521 | free(g_romdatabase.list->entry.refmd5); |
522 | free(g_romdatabase.list); |
523 | g_romdatabase.list = search; |
524 | } |
525 | } |
526 | |
527 | static romdatabase_entry* ini_search_by_md5(md5_byte_t* md5) |
528 | { |
529 | romdatabase_search* search; |
530 | |
531 | if(!g_romdatabase.have_database) |
532 | return NULL; |
533 | |
534 | search = g_romdatabase.md5_lists[md5[0]]; |
535 | |
536 | while (search != NULL && memcmp(search->entry.md5, md5, 16) != 0) |
537 | search = search->next_md5; |
538 | |
539 | if(search==NULL) |
540 | return NULL; |
541 | |
542 | return &(search->entry); |
543 | } |
544 | |
545 | romdatabase_entry* ini_search_by_crc(unsigned int crc1, unsigned int crc2) |
546 | { |
547 | romdatabase_search* search; |
548 | |
549 | if(!g_romdatabase.have_database) |
550 | return NULL; |
551 | |
552 | search = g_romdatabase.crc_lists[((crc1 >> 24) & 0xff)]; |
553 | |
554 | while (search != NULL && search->entry.crc1 != crc1 && search->entry.crc2 != crc2) |
555 | search = search->next_crc; |
556 | |
557 | if(search == NULL) |
558 | return NULL; |
559 | |
560 | return &(search->entry); |
561 | } |
562 | |
563 | |