1 | /**************************************************************************/ |
2 | /* stream_peer.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "stream_peer.h" |
32 | |
33 | #include "core/io/marshalls.h" |
34 | |
35 | Error StreamPeer::_put_data(const Vector<uint8_t> &p_data) { |
36 | int len = p_data.size(); |
37 | if (len == 0) { |
38 | return OK; |
39 | } |
40 | const uint8_t *r = p_data.ptr(); |
41 | return put_data(&r[0], len); |
42 | } |
43 | |
44 | Array StreamPeer::_put_partial_data(const Vector<uint8_t> &p_data) { |
45 | Array ret; |
46 | |
47 | int len = p_data.size(); |
48 | if (len == 0) { |
49 | ret.push_back(OK); |
50 | ret.push_back(0); |
51 | return ret; |
52 | } |
53 | |
54 | const uint8_t *r = p_data.ptr(); |
55 | int sent; |
56 | Error err = put_partial_data(&r[0], len, sent); |
57 | |
58 | if (err != OK) { |
59 | sent = 0; |
60 | } |
61 | ret.push_back(err); |
62 | ret.push_back(sent); |
63 | return ret; |
64 | } |
65 | |
66 | Array StreamPeer::_get_data(int p_bytes) { |
67 | Array ret; |
68 | |
69 | Vector<uint8_t> data; |
70 | data.resize(p_bytes); |
71 | if (data.size() != p_bytes) { |
72 | ret.push_back(ERR_OUT_OF_MEMORY); |
73 | ret.push_back(Vector<uint8_t>()); |
74 | return ret; |
75 | } |
76 | |
77 | uint8_t *w = data.ptrw(); |
78 | Error err = get_data(&w[0], p_bytes); |
79 | |
80 | ret.push_back(err); |
81 | ret.push_back(data); |
82 | return ret; |
83 | } |
84 | |
85 | Array StreamPeer::_get_partial_data(int p_bytes) { |
86 | Array ret; |
87 | |
88 | Vector<uint8_t> data; |
89 | data.resize(p_bytes); |
90 | if (data.size() != p_bytes) { |
91 | ret.push_back(ERR_OUT_OF_MEMORY); |
92 | ret.push_back(Vector<uint8_t>()); |
93 | return ret; |
94 | } |
95 | |
96 | uint8_t *w = data.ptrw(); |
97 | int received; |
98 | Error err = get_partial_data(&w[0], p_bytes, received); |
99 | |
100 | if (err != OK) { |
101 | data.clear(); |
102 | } else if (received != data.size()) { |
103 | data.resize(received); |
104 | } |
105 | |
106 | ret.push_back(err); |
107 | ret.push_back(data); |
108 | return ret; |
109 | } |
110 | |
111 | void StreamPeer::set_big_endian(bool p_big_endian) { |
112 | big_endian = p_big_endian; |
113 | } |
114 | |
115 | bool StreamPeer::is_big_endian_enabled() const { |
116 | return big_endian; |
117 | } |
118 | |
119 | void StreamPeer::put_u8(uint8_t p_val) { |
120 | put_data((const uint8_t *)&p_val, 1); |
121 | } |
122 | |
123 | void StreamPeer::put_8(int8_t p_val) { |
124 | put_data((const uint8_t *)&p_val, 1); |
125 | } |
126 | |
127 | void StreamPeer::put_u16(uint16_t p_val) { |
128 | if (big_endian) { |
129 | p_val = BSWAP16(p_val); |
130 | } |
131 | uint8_t buf[2]; |
132 | encode_uint16(p_val, buf); |
133 | put_data(buf, 2); |
134 | } |
135 | |
136 | void StreamPeer::put_16(int16_t p_val) { |
137 | if (big_endian) { |
138 | p_val = BSWAP16(p_val); |
139 | } |
140 | uint8_t buf[2]; |
141 | encode_uint16(p_val, buf); |
142 | put_data(buf, 2); |
143 | } |
144 | |
145 | void StreamPeer::put_u32(uint32_t p_val) { |
146 | if (big_endian) { |
147 | p_val = BSWAP32(p_val); |
148 | } |
149 | uint8_t buf[4]; |
150 | encode_uint32(p_val, buf); |
151 | put_data(buf, 4); |
152 | } |
153 | |
154 | void StreamPeer::put_32(int32_t p_val) { |
155 | if (big_endian) { |
156 | p_val = BSWAP32(p_val); |
157 | } |
158 | uint8_t buf[4]; |
159 | encode_uint32(p_val, buf); |
160 | put_data(buf, 4); |
161 | } |
162 | |
163 | void StreamPeer::put_u64(uint64_t p_val) { |
164 | if (big_endian) { |
165 | p_val = BSWAP64(p_val); |
166 | } |
167 | uint8_t buf[8]; |
168 | encode_uint64(p_val, buf); |
169 | put_data(buf, 8); |
170 | } |
171 | |
172 | void StreamPeer::put_64(int64_t p_val) { |
173 | if (big_endian) { |
174 | p_val = BSWAP64(p_val); |
175 | } |
176 | uint8_t buf[8]; |
177 | encode_uint64(p_val, buf); |
178 | put_data(buf, 8); |
179 | } |
180 | |
181 | void StreamPeer::put_float(float p_val) { |
182 | uint8_t buf[4]; |
183 | |
184 | encode_float(p_val, buf); |
185 | if (big_endian) { |
186 | uint32_t *p32 = (uint32_t *)buf; |
187 | *p32 = BSWAP32(*p32); |
188 | } |
189 | |
190 | put_data(buf, 4); |
191 | } |
192 | |
193 | void StreamPeer::put_double(double p_val) { |
194 | uint8_t buf[8]; |
195 | encode_double(p_val, buf); |
196 | if (big_endian) { |
197 | uint64_t *p64 = (uint64_t *)buf; |
198 | *p64 = BSWAP64(*p64); |
199 | } |
200 | put_data(buf, 8); |
201 | } |
202 | |
203 | void StreamPeer::put_string(const String &p_string) { |
204 | CharString cs = p_string.ascii(); |
205 | put_u32(cs.length()); |
206 | put_data((const uint8_t *)cs.get_data(), cs.length()); |
207 | } |
208 | |
209 | void StreamPeer::put_utf8_string(const String &p_string) { |
210 | CharString cs = p_string.utf8(); |
211 | put_u32(cs.length()); |
212 | put_data((const uint8_t *)cs.get_data(), cs.length()); |
213 | } |
214 | |
215 | void StreamPeer::put_var(const Variant &p_variant, bool p_full_objects) { |
216 | int len = 0; |
217 | Vector<uint8_t> buf; |
218 | encode_variant(p_variant, nullptr, len, p_full_objects); |
219 | buf.resize(len); |
220 | put_32(len); |
221 | encode_variant(p_variant, buf.ptrw(), len, p_full_objects); |
222 | put_data(buf.ptr(), buf.size()); |
223 | } |
224 | |
225 | uint8_t StreamPeer::get_u8() { |
226 | uint8_t buf[1]; |
227 | get_data(buf, 1); |
228 | return buf[0]; |
229 | } |
230 | |
231 | int8_t StreamPeer::get_8() { |
232 | uint8_t buf[1]; |
233 | get_data(buf, 1); |
234 | return buf[0]; |
235 | } |
236 | |
237 | uint16_t StreamPeer::get_u16() { |
238 | uint8_t buf[2]; |
239 | get_data(buf, 2); |
240 | uint16_t r = decode_uint16(buf); |
241 | if (big_endian) { |
242 | r = BSWAP16(r); |
243 | } |
244 | return r; |
245 | } |
246 | |
247 | int16_t StreamPeer::get_16() { |
248 | uint8_t buf[2]; |
249 | get_data(buf, 2); |
250 | uint16_t r = decode_uint16(buf); |
251 | if (big_endian) { |
252 | r = BSWAP16(r); |
253 | } |
254 | return r; |
255 | } |
256 | |
257 | uint32_t StreamPeer::get_u32() { |
258 | uint8_t buf[4]; |
259 | get_data(buf, 4); |
260 | uint32_t r = decode_uint32(buf); |
261 | if (big_endian) { |
262 | r = BSWAP32(r); |
263 | } |
264 | return r; |
265 | } |
266 | |
267 | int32_t StreamPeer::get_32() { |
268 | uint8_t buf[4]; |
269 | get_data(buf, 4); |
270 | uint32_t r = decode_uint32(buf); |
271 | if (big_endian) { |
272 | r = BSWAP32(r); |
273 | } |
274 | return r; |
275 | } |
276 | |
277 | uint64_t StreamPeer::get_u64() { |
278 | uint8_t buf[8]; |
279 | get_data(buf, 8); |
280 | uint64_t r = decode_uint64(buf); |
281 | if (big_endian) { |
282 | r = BSWAP64(r); |
283 | } |
284 | return r; |
285 | } |
286 | |
287 | int64_t StreamPeer::get_64() { |
288 | uint8_t buf[8]; |
289 | get_data(buf, 8); |
290 | uint64_t r = decode_uint64(buf); |
291 | if (big_endian) { |
292 | r = BSWAP64(r); |
293 | } |
294 | return r; |
295 | } |
296 | |
297 | float StreamPeer::get_float() { |
298 | uint8_t buf[4]; |
299 | get_data(buf, 4); |
300 | |
301 | if (big_endian) { |
302 | uint32_t *p32 = (uint32_t *)buf; |
303 | *p32 = BSWAP32(*p32); |
304 | } |
305 | |
306 | return decode_float(buf); |
307 | } |
308 | |
309 | double StreamPeer::get_double() { |
310 | uint8_t buf[8]; |
311 | get_data(buf, 8); |
312 | |
313 | if (big_endian) { |
314 | uint64_t *p64 = (uint64_t *)buf; |
315 | *p64 = BSWAP64(*p64); |
316 | } |
317 | |
318 | return decode_double(buf); |
319 | } |
320 | |
321 | String StreamPeer::get_string(int p_bytes) { |
322 | if (p_bytes < 0) { |
323 | p_bytes = get_u32(); |
324 | } |
325 | ERR_FAIL_COND_V(p_bytes < 0, String()); |
326 | |
327 | Vector<char> buf; |
328 | Error err = buf.resize(p_bytes + 1); |
329 | ERR_FAIL_COND_V(err != OK, String()); |
330 | err = get_data((uint8_t *)&buf[0], p_bytes); |
331 | ERR_FAIL_COND_V(err != OK, String()); |
332 | buf.write[p_bytes] = 0; |
333 | return buf.ptr(); |
334 | } |
335 | |
336 | String StreamPeer::get_utf8_string(int p_bytes) { |
337 | if (p_bytes < 0) { |
338 | p_bytes = get_u32(); |
339 | } |
340 | ERR_FAIL_COND_V(p_bytes < 0, String()); |
341 | |
342 | Vector<uint8_t> buf; |
343 | Error err = buf.resize(p_bytes); |
344 | ERR_FAIL_COND_V(err != OK, String()); |
345 | err = get_data(buf.ptrw(), p_bytes); |
346 | ERR_FAIL_COND_V(err != OK, String()); |
347 | |
348 | String ret; |
349 | ret.parse_utf8((const char *)buf.ptr(), buf.size()); |
350 | return ret; |
351 | } |
352 | |
353 | Variant StreamPeer::get_var(bool p_allow_objects) { |
354 | int len = get_32(); |
355 | Vector<uint8_t> var; |
356 | Error err = var.resize(len); |
357 | ERR_FAIL_COND_V(err != OK, Variant()); |
358 | err = get_data(var.ptrw(), len); |
359 | ERR_FAIL_COND_V(err != OK, Variant()); |
360 | |
361 | Variant ret; |
362 | err = decode_variant(ret, var.ptr(), len, nullptr, p_allow_objects); |
363 | ERR_FAIL_COND_V_MSG(err != OK, Variant(), "Error when trying to decode Variant." ); |
364 | |
365 | return ret; |
366 | } |
367 | |
368 | void StreamPeer::_bind_methods() { |
369 | ClassDB::bind_method(D_METHOD("put_data" , "data" ), &StreamPeer::_put_data); |
370 | ClassDB::bind_method(D_METHOD("put_partial_data" , "data" ), &StreamPeer::_put_partial_data); |
371 | |
372 | ClassDB::bind_method(D_METHOD("get_data" , "bytes" ), &StreamPeer::_get_data); |
373 | ClassDB::bind_method(D_METHOD("get_partial_data" , "bytes" ), &StreamPeer::_get_partial_data); |
374 | |
375 | ClassDB::bind_method(D_METHOD("get_available_bytes" ), &StreamPeer::get_available_bytes); |
376 | |
377 | ClassDB::bind_method(D_METHOD("set_big_endian" , "enable" ), &StreamPeer::set_big_endian); |
378 | ClassDB::bind_method(D_METHOD("is_big_endian_enabled" ), &StreamPeer::is_big_endian_enabled); |
379 | |
380 | ClassDB::bind_method(D_METHOD("put_8" , "value" ), &StreamPeer::put_8); |
381 | ClassDB::bind_method(D_METHOD("put_u8" , "value" ), &StreamPeer::put_u8); |
382 | ClassDB::bind_method(D_METHOD("put_16" , "value" ), &StreamPeer::put_16); |
383 | ClassDB::bind_method(D_METHOD("put_u16" , "value" ), &StreamPeer::put_u16); |
384 | ClassDB::bind_method(D_METHOD("put_32" , "value" ), &StreamPeer::put_32); |
385 | ClassDB::bind_method(D_METHOD("put_u32" , "value" ), &StreamPeer::put_u32); |
386 | ClassDB::bind_method(D_METHOD("put_64" , "value" ), &StreamPeer::put_64); |
387 | ClassDB::bind_method(D_METHOD("put_u64" , "value" ), &StreamPeer::put_u64); |
388 | ClassDB::bind_method(D_METHOD("put_float" , "value" ), &StreamPeer::put_float); |
389 | ClassDB::bind_method(D_METHOD("put_double" , "value" ), &StreamPeer::put_double); |
390 | ClassDB::bind_method(D_METHOD("put_string" , "value" ), &StreamPeer::put_string); |
391 | ClassDB::bind_method(D_METHOD("put_utf8_string" , "value" ), &StreamPeer::put_utf8_string); |
392 | ClassDB::bind_method(D_METHOD("put_var" , "value" , "full_objects" ), &StreamPeer::put_var, DEFVAL(false)); |
393 | |
394 | ClassDB::bind_method(D_METHOD("get_8" ), &StreamPeer::get_8); |
395 | ClassDB::bind_method(D_METHOD("get_u8" ), &StreamPeer::get_u8); |
396 | ClassDB::bind_method(D_METHOD("get_16" ), &StreamPeer::get_16); |
397 | ClassDB::bind_method(D_METHOD("get_u16" ), &StreamPeer::get_u16); |
398 | ClassDB::bind_method(D_METHOD("get_32" ), &StreamPeer::get_32); |
399 | ClassDB::bind_method(D_METHOD("get_u32" ), &StreamPeer::get_u32); |
400 | ClassDB::bind_method(D_METHOD("get_64" ), &StreamPeer::get_64); |
401 | ClassDB::bind_method(D_METHOD("get_u64" ), &StreamPeer::get_u64); |
402 | ClassDB::bind_method(D_METHOD("get_float" ), &StreamPeer::get_float); |
403 | ClassDB::bind_method(D_METHOD("get_double" ), &StreamPeer::get_double); |
404 | ClassDB::bind_method(D_METHOD("get_string" , "bytes" ), &StreamPeer::get_string, DEFVAL(-1)); |
405 | ClassDB::bind_method(D_METHOD("get_utf8_string" , "bytes" ), &StreamPeer::get_utf8_string, DEFVAL(-1)); |
406 | ClassDB::bind_method(D_METHOD("get_var" , "allow_objects" ), &StreamPeer::get_var, DEFVAL(false)); |
407 | |
408 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "big_endian" ), "set_big_endian" , "is_big_endian_enabled" ); |
409 | } |
410 | |
411 | //////////////////////////////// |
412 | |
413 | Error StreamPeerExtension::get_data(uint8_t *r_buffer, int p_bytes) { |
414 | Error err; |
415 | int received = 0; |
416 | if (GDVIRTUAL_CALL(_get_data, r_buffer, p_bytes, &received, err)) { |
417 | return err; |
418 | } |
419 | WARN_PRINT_ONCE("StreamPeerExtension::_get_data is unimplemented!" ); |
420 | return FAILED; |
421 | } |
422 | |
423 | Error StreamPeerExtension::get_partial_data(uint8_t *r_buffer, int p_bytes, int &r_received) { |
424 | Error err; |
425 | if (GDVIRTUAL_CALL(_get_partial_data, r_buffer, p_bytes, &r_received, err)) { |
426 | return err; |
427 | } |
428 | WARN_PRINT_ONCE("StreamPeerExtension::_get_partial_data is unimplemented!" ); |
429 | return FAILED; |
430 | } |
431 | |
432 | Error StreamPeerExtension::put_data(const uint8_t *p_data, int p_bytes) { |
433 | Error err; |
434 | int sent = 0; |
435 | if (GDVIRTUAL_CALL(_put_data, p_data, p_bytes, &sent, err)) { |
436 | return err; |
437 | } |
438 | WARN_PRINT_ONCE("StreamPeerExtension::_put_data is unimplemented!" ); |
439 | return FAILED; |
440 | } |
441 | |
442 | Error StreamPeerExtension::put_partial_data(const uint8_t *p_data, int p_bytes, int &r_sent) { |
443 | Error err; |
444 | if (GDVIRTUAL_CALL(_put_data, p_data, p_bytes, &r_sent, err)) { |
445 | return err; |
446 | } |
447 | WARN_PRINT_ONCE("StreamPeerExtension::_put_partial_data is unimplemented!" ); |
448 | return FAILED; |
449 | } |
450 | |
451 | void StreamPeerExtension::_bind_methods() { |
452 | GDVIRTUAL_BIND(_get_data, "r_buffer" , "r_bytes" , "r_received" ); |
453 | GDVIRTUAL_BIND(_get_partial_data, "r_buffer" , "r_bytes" , "r_received" ); |
454 | GDVIRTUAL_BIND(_put_data, "p_data" , "p_bytes" , "r_sent" ); |
455 | GDVIRTUAL_BIND(_put_partial_data, "p_data" , "p_bytes" , "r_sent" ); |
456 | GDVIRTUAL_BIND(_get_available_bytes); |
457 | } |
458 | |
459 | //////////////////////////////// |
460 | |
461 | void StreamPeerBuffer::_bind_methods() { |
462 | ClassDB::bind_method(D_METHOD("seek" , "position" ), &StreamPeerBuffer::seek); |
463 | ClassDB::bind_method(D_METHOD("get_size" ), &StreamPeerBuffer::get_size); |
464 | ClassDB::bind_method(D_METHOD("get_position" ), &StreamPeerBuffer::get_position); |
465 | ClassDB::bind_method(D_METHOD("resize" , "size" ), &StreamPeerBuffer::resize); |
466 | ClassDB::bind_method(D_METHOD("set_data_array" , "data" ), &StreamPeerBuffer::set_data_array); |
467 | ClassDB::bind_method(D_METHOD("get_data_array" ), &StreamPeerBuffer::get_data_array); |
468 | ClassDB::bind_method(D_METHOD("clear" ), &StreamPeerBuffer::clear); |
469 | ClassDB::bind_method(D_METHOD("duplicate" ), &StreamPeerBuffer::duplicate); |
470 | |
471 | ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "data_array" ), "set_data_array" , "get_data_array" ); |
472 | } |
473 | |
474 | Error StreamPeerBuffer::put_data(const uint8_t *p_data, int p_bytes) { |
475 | if (p_bytes <= 0) { |
476 | return OK; |
477 | } |
478 | |
479 | if (pointer + p_bytes > data.size()) { |
480 | data.resize(pointer + p_bytes); |
481 | } |
482 | |
483 | uint8_t *w = data.ptrw(); |
484 | memcpy(&w[pointer], p_data, p_bytes); |
485 | |
486 | pointer += p_bytes; |
487 | return OK; |
488 | } |
489 | |
490 | Error StreamPeerBuffer::put_partial_data(const uint8_t *p_data, int p_bytes, int &r_sent) { |
491 | r_sent = p_bytes; |
492 | return put_data(p_data, p_bytes); |
493 | } |
494 | |
495 | Error StreamPeerBuffer::get_data(uint8_t *p_buffer, int p_bytes) { |
496 | int recv; |
497 | get_partial_data(p_buffer, p_bytes, recv); |
498 | if (recv != p_bytes) { |
499 | return ERR_INVALID_PARAMETER; |
500 | } |
501 | |
502 | return OK; |
503 | } |
504 | |
505 | Error StreamPeerBuffer::get_partial_data(uint8_t *p_buffer, int p_bytes, int &r_received) { |
506 | if (pointer + p_bytes > data.size()) { |
507 | r_received = data.size() - pointer; |
508 | if (r_received <= 0) { |
509 | r_received = 0; |
510 | return OK; //you got 0 |
511 | } |
512 | } else { |
513 | r_received = p_bytes; |
514 | } |
515 | |
516 | const uint8_t *r = data.ptr(); |
517 | memcpy(p_buffer, r + pointer, r_received); |
518 | |
519 | pointer += r_received; |
520 | // FIXME: return what? OK or ERR_* |
521 | // return OK for now so we don't maybe return garbage |
522 | return OK; |
523 | } |
524 | |
525 | int StreamPeerBuffer::get_available_bytes() const { |
526 | return data.size() - pointer; |
527 | } |
528 | |
529 | void StreamPeerBuffer::seek(int p_pos) { |
530 | ERR_FAIL_COND(p_pos < 0); |
531 | ERR_FAIL_COND(p_pos > data.size()); |
532 | pointer = p_pos; |
533 | } |
534 | |
535 | int StreamPeerBuffer::get_size() const { |
536 | return data.size(); |
537 | } |
538 | |
539 | int StreamPeerBuffer::get_position() const { |
540 | return pointer; |
541 | } |
542 | |
543 | void StreamPeerBuffer::resize(int p_size) { |
544 | data.resize(p_size); |
545 | } |
546 | |
547 | void StreamPeerBuffer::set_data_array(const Vector<uint8_t> &p_data) { |
548 | data = p_data; |
549 | pointer = 0; |
550 | } |
551 | |
552 | Vector<uint8_t> StreamPeerBuffer::get_data_array() const { |
553 | return data; |
554 | } |
555 | |
556 | void StreamPeerBuffer::clear() { |
557 | data.clear(); |
558 | pointer = 0; |
559 | } |
560 | |
561 | Ref<StreamPeerBuffer> StreamPeerBuffer::duplicate() const { |
562 | Ref<StreamPeerBuffer> spb; |
563 | spb.instantiate(); |
564 | spb->data = data; |
565 | return spb; |
566 | } |
567 | |