Line data Source code
1 : /*
2 : This program is free software: you can redistribute it and/or modify
3 : it under the terms of the GNU General Public License as published by
4 : the Free Software Foundation, either version 3 of the License, or
5 : (at your option) any later version.
6 :
7 : This program is distributed in the hope that it will be useful,
8 : but WITHOUT ANY WARRANTY; without even the implied warranty of
9 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 : GNU General Public License for more details.
11 :
12 : You should have received a copy of the GNU General Public License
13 : along with this program. If not, see <http://www.gnu.org/licenses/>.
14 : */
15 : /*
16 : ArduPilot filesystem interface for parameters
17 : */
18 :
19 : #include "AP_Filesystem_config.h"
20 :
21 : #if AP_FILESYSTEM_PARAM_ENABLED
22 :
23 : #include "AP_Filesystem.h"
24 : #include "AP_Filesystem_Param.h"
25 : #include <AP_Param/AP_Param.h>
26 : #include <AP_Math/AP_Math.h>
27 : #include <ctype.h>
28 :
29 : #define PACKED_NAME "param.pck"
30 :
31 : extern const AP_HAL::HAL& hal;
32 :
33 : // QURT HAL already has a declaration of errno in errno.h
34 : #if CONFIG_HAL_BOARD != HAL_BOARD_QURT
35 : extern int errno;
36 : #endif
37 :
38 3 : int AP_Filesystem_Param::open(const char *fname, int flags, bool allow_absolute_path)
39 : {
40 3 : if (!check_file_name(fname)) {
41 0 : errno = ENOENT;
42 0 : return -1;
43 : }
44 3 : bool read_only = ((flags & O_ACCMODE) == O_RDONLY);
45 : uint8_t idx;
46 3 : for (idx=0; idx<max_open_file; idx++) {
47 3 : if (!file[idx].open) {
48 3 : break;
49 : }
50 : }
51 3 : if (idx == max_open_file) {
52 0 : errno = ENFILE;
53 0 : return -1;
54 : }
55 3 : struct rfile &r = file[idx];
56 3 : if (read_only) {
57 2 : r.cursors = NEW_NOTHROW cursor[num_cursors];
58 2 : if (r.cursors == nullptr) {
59 0 : errno = ENOMEM;
60 0 : return -1;
61 : }
62 : }
63 3 : r.file_ofs = 0;
64 3 : r.open = true;
65 3 : r.with_defaults = false;
66 3 : r.start = 0;
67 3 : r.count = 0;
68 3 : r.read_size = 0;
69 3 : r.file_size = 0;
70 3 : r.writebuf = nullptr;
71 3 : if (!read_only) {
72 : // setup for upload
73 1 : r.writebuf = NEW_NOTHROW ExpandingString();
74 1 : if (r.writebuf == nullptr) {
75 0 : close(idx);
76 0 : errno = ENOMEM;
77 0 : return -1;
78 : }
79 : }
80 :
81 : /*
82 : allow for URI style arguments param.pck?start=N&count=C
83 : */
84 3 : const char *c = strchr(fname, '?');
85 4 : while (c && *c) {
86 1 : c++;
87 1 : if (strncmp(c, "start=", 6) == 0) {
88 0 : uint32_t v = strtoul(c+6, nullptr, 10);
89 0 : if (v >= UINT16_MAX) {
90 0 : goto failed;
91 : }
92 0 : r.start = v;
93 0 : c += 6;
94 0 : c = strchr(c, '&');
95 0 : continue;
96 0 : }
97 1 : if (strncmp(c, "count=", 6) == 0) {
98 0 : uint32_t v = strtoul(c+6, nullptr, 10);
99 0 : if (v >= UINT16_MAX) {
100 0 : goto failed;
101 : }
102 0 : r.count = v;
103 0 : c += 6;
104 0 : c = strchr(c, '&');
105 0 : continue;
106 0 : }
107 : #if AP_PARAM_DEFAULTS_ENABLED
108 1 : if (strncmp(c, "withdefaults=", 13) == 0) {
109 1 : uint32_t v = strtoul(c+13, nullptr, 10);
110 1 : if (v > 1) {
111 0 : goto failed;
112 : }
113 1 : r.with_defaults = v == 1;
114 1 : c += 13;
115 1 : c = strchr(c, '&');
116 1 : continue;
117 1 : }
118 : #endif
119 : }
120 :
121 3 : return idx;
122 :
123 0 : failed:
124 0 : delete [] r.cursors;
125 0 : r.open = false;
126 0 : errno = EINVAL;
127 0 : return -1;
128 : }
129 :
130 3 : int AP_Filesystem_Param::close(int fd)
131 : {
132 3 : if (fd < 0 || fd >= max_open_file || !file[fd].open) {
133 0 : errno = EBADF;
134 0 : return -1;
135 : }
136 3 : struct rfile &r = file[fd];
137 3 : int ret = 0;
138 3 : if (r.writebuf != nullptr && !finish_upload(r)) {
139 0 : errno = EINVAL;
140 0 : ret = -1;
141 : }
142 3 : r.open = false;
143 3 : delete [] r.cursors;
144 3 : r.cursors = nullptr;
145 3 : delete r.writebuf;
146 3 : r.writebuf = nullptr;
147 3 : return ret;
148 : }
149 :
150 : /*
151 : packed format:
152 : file header:
153 : uint16_t magic = 0x671b or 0x671c for included default values
154 : uint16_t num_params
155 : uint16_t total_params
156 :
157 : per-parameter:
158 :
159 : uint8_t type:4; // AP_Param type NONE=0, INT8=1, INT16=2, INT32=3, FLOAT=4
160 : uint8_t flags:4; // bit 0: includes default value for this param
161 : uint8_t common_len:4; // number of name bytes in common with previous entry, 0..15
162 : uint8_t name_len:4; // non-common length of param name -1 (0..15)
163 : uint8_t name[name_len]; // name
164 : uint8_t data[]; // value, length given by variable type, data length doubled if default is included
165 :
166 : Any leading zero bytes after the header should be discarded as pad
167 : bytes. Pad bytes are used to ensure that a parameter data[] field
168 : does not cross a read packet boundary
169 : */
170 :
171 : /*
172 : pack a single parameter. The buffer must be at least of size max_pack_len
173 : */
174 3144 : uint8_t AP_Filesystem_Param::pack_param(const struct rfile &r, struct cursor &c, uint8_t *buf)
175 : {
176 : char name[AP_MAX_NAME_SIZE+1];
177 3144 : name[AP_MAX_NAME_SIZE] = 0;
178 : enum ap_var_type ptype;
179 : AP_Param *ap;
180 : float default_val;
181 :
182 3144 : if (c.token_ofs == 0) {
183 2 : c.idx = 0;
184 2 : ap = AP_Param::first(&c.token, &ptype, &default_val);
185 2 : uint16_t idx = 0;
186 2 : while (idx < r.start && ap) {
187 0 : ap = AP_Param::next_scalar(&c.token, &ptype, &default_val);
188 0 : idx++;
189 : }
190 : } else {
191 3142 : c.idx++;
192 3142 : ap = AP_Param::next_scalar(&c.token, &ptype, &default_val);
193 : }
194 3144 : if (ap == nullptr || (r.count && c.idx >= r.count)) {
195 2 : if (r.count == 0 && c.idx != AP_Param::count_parameters()) {
196 : // the parameter count is incorrect, invalidate so a
197 : // repeated param download avoids an error
198 0 : AP_Param::invalidate_count();
199 : }
200 2 : return 0;
201 : }
202 3142 : ap->copy_name_token(c.token, name, AP_MAX_NAME_SIZE, true);
203 :
204 3142 : uint8_t common_len = 0;
205 3142 : const char *last_name = c.last_name;
206 3142 : const char *pname = name;
207 24226 : while (*pname == *last_name && *pname) {
208 21084 : common_len++;
209 21084 : pname++;
210 21084 : last_name++;
211 : }
212 3142 : uint8_t name_len = strlen(pname);
213 3142 : if (name_len == 0) {
214 0 : name_len = 1;
215 0 : common_len--;
216 0 : pname--;
217 : }
218 : #if AP_PARAM_DEFAULTS_ENABLED
219 3142 : const bool add_default = r.with_defaults && !is_equal(ap->cast_to_float(ptype), default_val);
220 : #else
221 : const bool add_default = false;
222 : #endif
223 3142 : const uint8_t type_len = AP_Param::type_size(ptype);
224 3142 : uint8_t packed_len = type_len + name_len + 2 + (add_default ? type_len : 0);
225 3142 : const uint8_t flags = add_default;
226 :
227 : /*
228 : see if we need to add padding to ensure that a data field never
229 : crosses a block boundary. This ensures that re-reading a block
230 : won't get a corrupt value for a parameter
231 : */
232 3142 : if (type_len > 1) {
233 2151 : const uint32_t ofs = c.token_ofs + sizeof(struct header) + packed_len;
234 2151 : const uint32_t ofs_mod = ofs % r.read_size;
235 2151 : if (ofs_mod > 0 && ofs_mod < type_len) {
236 24 : const uint8_t pad = type_len - ofs_mod;
237 24 : memset(buf, 0, pad);
238 24 : buf += pad;
239 24 : packed_len += pad;
240 : }
241 : }
242 :
243 3142 : buf[0] = uint8_t(ptype) | (flags<<4);
244 3142 : buf[1] = common_len | ((name_len-1)<<4);
245 3142 : memcpy(&buf[2], pname, name_len);
246 3142 : memcpy(&buf[2+name_len], ap, type_len);
247 : #if AP_PARAM_DEFAULTS_ENABLED
248 3142 : if (add_default) {
249 47 : switch (ptype) {
250 0 : case AP_PARAM_NONE:
251 : case AP_PARAM_GROUP:
252 : // should never happen...
253 0 : break;
254 3 : case AP_PARAM_INT8: {
255 3 : const int32_t int8_default = default_val;
256 3 : memcpy(&buf[2+name_len+type_len], &int8_default, type_len);
257 3 : break;
258 : }
259 12 : case AP_PARAM_INT16: {
260 12 : const int16_t int16_default = default_val;
261 12 : memcpy(&buf[2+name_len+type_len], &int16_default, type_len);
262 12 : break;
263 : }
264 19 : case AP_PARAM_INT32: {
265 19 : const int32_t int32_default = default_val;
266 19 : memcpy(&buf[2+name_len+type_len], &int32_default, type_len);
267 19 : break;
268 : }
269 13 : case AP_PARAM_FLOAT:
270 : case AP_PARAM_VECTOR3F: {
271 13 : memcpy(&buf[2+name_len+type_len], &default_val, type_len);
272 13 : break;
273 : }
274 : }
275 : }
276 : #endif
277 :
278 3142 : strcpy(c.last_name, name);
279 :
280 3142 : return packed_len;
281 : }
282 :
283 : /*
284 : seek the token to match file offset
285 : */
286 0 : bool AP_Filesystem_Param::token_seek(const struct rfile &r, const uint32_t data_ofs, struct cursor &c)
287 : {
288 0 : if (data_ofs == 0) {
289 0 : memset(&c, 0, sizeof(c));
290 0 : return true;
291 : }
292 0 : if (c.token_ofs > data_ofs) {
293 0 : memset(&c, 0, sizeof(c));
294 : }
295 :
296 0 : if (c.trailer_len > 0) {
297 0 : uint8_t n = MIN(c.trailer_len, data_ofs - c.token_ofs);
298 0 : if (n != c.trailer_len) {
299 0 : memmove(&c.trailer[0], &c.trailer[n], c.trailer_len - n);
300 : }
301 0 : c.trailer_len -= n;
302 0 : c.token_ofs += n;
303 : }
304 :
305 0 : while (data_ofs != c.token_ofs) {
306 0 : uint32_t available = data_ofs - c.token_ofs;
307 : uint8_t tbuf[max_pack_len];
308 0 : uint8_t len = pack_param(r, c, tbuf);
309 0 : if (len == 0) {
310 : // no more parameters
311 0 : break;
312 : }
313 0 : uint8_t n = MIN(len, available);
314 0 : if (len > available) {
315 0 : c.trailer_len = len - available;
316 0 : memcpy(c.trailer, &tbuf[n], c.trailer_len);
317 : }
318 0 : c.token_ofs += n;
319 : }
320 0 : return data_ofs == c.token_ofs;
321 : }
322 :
323 150 : int32_t AP_Filesystem_Param::read(int fd, void *buf, uint32_t count)
324 : {
325 150 : if (fd < 0 || fd >= max_open_file || !file[fd].open) {
326 0 : errno = EBADF;
327 0 : return -1;
328 : }
329 :
330 150 : struct rfile &r = file[fd];
331 150 : if (r.writebuf != nullptr) {
332 : // no read on upload
333 0 : errno = EINVAL;
334 0 : return -1;
335 : }
336 150 : size_t header_total = 0;
337 :
338 : /*
339 : we only allow for a single read size. This ensures that pad
340 : bytes placed to avoid a data value crossing a block boundary in
341 : the ftp protocol do not change when filling in lost packets
342 : */
343 150 : if (r.read_size == 0 && count > 0) {
344 2 : r.read_size = count;
345 : }
346 150 : if (r.read_size != 0 && r.read_size != count) {
347 0 : errno = EINVAL;
348 0 : return -1;
349 : }
350 :
351 150 : if (r.file_size != 0) {
352 : // ensure we don't try to read past EOF
353 2 : if (r.file_ofs > r.file_size) {
354 0 : count = 0;
355 : } else {
356 2 : count = MIN(count, r.file_size - r.file_ofs);
357 : }
358 : }
359 :
360 150 : if (r.file_ofs < sizeof(struct header)) {
361 2 : struct header hdr;
362 2 : hdr.total_params = AP_Param::count_parameters();
363 2 : if (hdr.total_params <= r.start) {
364 0 : errno = EINVAL;
365 0 : return -1;
366 : }
367 2 : hdr.num_params = hdr.total_params - r.start;
368 2 : if (r.count > 0 && hdr.num_params > r.count) {
369 0 : hdr.num_params = r.count;
370 : }
371 2 : uint8_t n = MIN(sizeof(hdr) - r.file_ofs, count);
372 2 : if (r.with_defaults) {
373 1 : hdr.magic = pmagic_with_default;
374 : }
375 2 : const uint8_t *b = (const uint8_t *)&hdr;
376 2 : memcpy(buf, &b[r.file_ofs], n);
377 2 : count -= n;
378 2 : header_total += n;
379 2 : r.file_ofs += n;
380 2 : buf = (void *)(n + (const uint8_t *)buf);
381 2 : if (count == 0) {
382 0 : return header_total;
383 : }
384 : }
385 :
386 150 : uint32_t data_ofs = r.file_ofs - sizeof(struct header);
387 150 : uint8_t best_i = 0;
388 150 : uint32_t best_ofs = r.cursors[0].token_ofs;
389 150 : size_t total = 0;
390 :
391 : // find the first cursor that is positioned after the file offset
392 300 : for (uint8_t i=1; i<num_cursors; i++) {
393 150 : struct cursor &c = r.cursors[i];
394 150 : if (c.token_ofs >= data_ofs && c.token_ofs < best_ofs) {
395 0 : best_i = i;
396 0 : best_ofs = c.token_ofs;
397 : }
398 : }
399 150 : struct cursor &c = r.cursors[best_i];
400 :
401 150 : if (data_ofs != c.token_ofs) {
402 0 : if (!token_seek(r, data_ofs, c)) {
403 : // must be EOF
404 0 : return header_total;
405 : }
406 : }
407 150 : if (count == 0) {
408 2 : return header_total;
409 : }
410 148 : uint8_t *ubuf = (uint8_t *)buf;
411 :
412 148 : if (c.trailer_len > 0) {
413 134 : uint8_t n = MIN(c.trailer_len, count);
414 134 : memcpy(ubuf, c.trailer, n);
415 134 : count -= n;
416 134 : ubuf += n;
417 134 : if (n != c.trailer_len) {
418 0 : memmove(&c.trailer[0], &c.trailer[n], c.trailer_len - n);
419 : }
420 134 : c.trailer_len -= n;
421 134 : total += n;
422 134 : c.token_ofs += n;
423 : }
424 :
425 3290 : while (count > 0) {
426 : uint8_t tbuf[max_pack_len];
427 3144 : uint8_t len = pack_param(r, c, tbuf);
428 3144 : if (len == 0) {
429 : // no more params, use this to trigger EOF in later reads
430 2 : const uint32_t size = r.file_ofs + total;
431 2 : if (r.file_size == 0) {
432 2 : r.file_size = size;
433 : } else {
434 0 : r.file_size = MIN(size, r.file_size);
435 : }
436 2 : break;
437 : }
438 3142 : uint8_t n = MIN(len, count);
439 3142 : if (len > count) {
440 134 : c.trailer_len = len - count;
441 134 : memcpy(c.trailer, &tbuf[count], c.trailer_len);
442 : }
443 3142 : memcpy(ubuf, tbuf, n);
444 3142 : count -= n;
445 3142 : ubuf += n;
446 3142 : total += n;
447 3142 : c.token_ofs += n;
448 : }
449 148 : r.file_ofs += total;
450 148 : return total + header_total;
451 : }
452 :
453 5 : int32_t AP_Filesystem_Param::lseek(int fd, int32_t offset, int seek_from)
454 : {
455 5 : if (fd < 0 || fd >= max_open_file || !file[fd].open) {
456 0 : errno = EBADF;
457 0 : return -1;
458 : }
459 5 : struct rfile &r = file[fd];
460 5 : switch (seek_from) {
461 5 : case SEEK_SET:
462 5 : r.file_ofs = offset;
463 5 : break;
464 0 : case SEEK_CUR:
465 0 : r.file_ofs += offset;
466 0 : break;
467 0 : case SEEK_END:
468 0 : errno = EINVAL;
469 0 : return -1;
470 : }
471 5 : return r.file_ofs;
472 : }
473 :
474 2 : int AP_Filesystem_Param::stat(const char *name, struct stat *stbuf)
475 : {
476 2 : if (!check_file_name(name)) {
477 0 : errno = ENOENT;
478 0 : return -1;
479 : }
480 2 : memset(stbuf, 0, sizeof(*stbuf));
481 : // give size estimation to avoid needing to scan entire file
482 2 : stbuf->st_size = AP_Param::count_parameters() * 12;
483 2 : return 0;
484 : }
485 :
486 : /*
487 : check for the right file name
488 : */
489 5 : bool AP_Filesystem_Param::check_file_name(const char *name)
490 : {
491 5 : const uint8_t packed_len = strlen(PACKED_NAME);
492 5 : if (strncmp(name, PACKED_NAME, packed_len) == 0 &&
493 5 : (name[packed_len] == 0 || name[packed_len] == '?')) {
494 5 : return true;
495 : }
496 0 : return false;
497 : }
498 :
499 : /*
500 : support param upload
501 : */
502 3 : int32_t AP_Filesystem_Param::write(int fd, const void *buf, uint32_t count)
503 : {
504 3 : if (fd < 0 || fd >= max_open_file || !file[fd].open) {
505 0 : errno = EBADF;
506 0 : return -1;
507 : }
508 3 : struct rfile &r = file[fd];
509 3 : if (r.writebuf == nullptr) {
510 0 : errno = EBADF;
511 0 : return -1;
512 : }
513 3 : struct header hdr;
514 3 : if (r.file_ofs == 0 && count >= sizeof(hdr)) {
515 : // pre-expand the buffer to the full size when we get the header
516 3 : memcpy(&hdr, buf, sizeof(hdr));
517 3 : if (hdr.magic == pmagic) {
518 3 : const uint32_t flen = hdr.total_params;
519 3 : if (flen > r.writebuf->get_length()) {
520 1 : if (!r.writebuf->append(nullptr, flen - r.writebuf->get_length())) {
521 : // not enough memory
522 0 : return -1;
523 : }
524 : }
525 : }
526 : }
527 3 : if (r.file_ofs + count > r.writebuf->get_length()) {
528 0 : if (!r.writebuf->append(nullptr, r.file_ofs + count - r.writebuf->get_length())) {
529 0 : return -1;
530 : }
531 : }
532 3 : uint8_t *b = (uint8_t *)r.writebuf->get_writeable_string();
533 3 : memcpy(&b[r.file_ofs], buf, count);
534 3 : r.file_ofs += count;
535 3 : return count;
536 : }
537 :
538 : /*
539 : parse incoming parameters
540 : */
541 1 : bool AP_Filesystem_Param::param_upload_parse(const rfile &r, bool &need_retry)
542 : {
543 1 : need_retry = false;
544 :
545 1 : const uint8_t *b = (const uint8_t *)r.writebuf->get_string();
546 1 : uint32_t length = r.writebuf->get_length();
547 1 : struct header hdr;
548 1 : if (length < sizeof(hdr)) {
549 0 : return false;
550 : }
551 1 : memcpy(&hdr, b, sizeof(hdr));
552 1 : if (hdr.magic != pmagic) {
553 0 : return false;
554 : }
555 1 : if (length != hdr.total_params) {
556 0 : return false;
557 : }
558 1 : b += sizeof(hdr);
559 :
560 1 : char last_name[17] {};
561 :
562 3 : for (uint16_t i=0; i<hdr.num_params; i++) {
563 2 : enum ap_var_type ptype = (enum ap_var_type)(b[0]&0x0F);
564 2 : uint8_t flags = (enum ap_var_type)(b[0]>>4);
565 2 : if (flags != 0) {
566 0 : return false;
567 : }
568 2 : uint8_t common_len = b[1]&0xF;
569 2 : uint8_t name_len = (b[1]>>4)+1;
570 2 : if (common_len + name_len > 16) {
571 0 : return false;
572 : }
573 : char name[17];
574 2 : memcpy(name, last_name, common_len);
575 2 : memcpy(&name[common_len], &b[2], name_len);
576 2 : name[common_len+name_len] = 0;
577 :
578 2 : memcpy(last_name, name, sizeof(name));
579 2 : enum ap_var_type ptype2 = AP_PARAM_NONE;
580 : uint16_t flags2;
581 :
582 2 : b += 2 + name_len;
583 :
584 : // assume that this write to the Param filesystem is coming
585 : // via mavftp:
586 2 : AP_Param *p = AP_Param::find(name, &ptype2, &flags2);
587 2 : if (p == nullptr || !p->allow_set_via_mavlink(flags2)) {
588 1 : if (ptype == AP_PARAM_INT8) {
589 1 : b++;
590 0 : } else if (ptype == AP_PARAM_INT16) {
591 0 : b += 2;
592 : } else {
593 0 : b += 4;
594 : }
595 1 : continue;
596 : }
597 :
598 : /*
599 : if we are enabling a subsystem we need a small delay between
600 : parameters to allow main thread to perform any allocation of
601 : backends
602 : */
603 1 : bool need_delay = ((flags2 & AP_PARAM_FLAG_ENABLE) != 0 &&
604 1 : ptype2 == AP_PARAM_INT8 &&
605 0 : ((AP_Int8 *)p)->get() == 0);
606 :
607 1 : if (ptype == ptype2 && ptype == AP_PARAM_INT32) {
608 : // special handling of int32_t to preserve all bits
609 : int32_t v;
610 0 : memcpy(&v, b, sizeof(v));
611 0 : ((AP_Int32 *)p)->set(v);
612 0 : b += 4;
613 1 : } else if (ptype == AP_PARAM_INT8) {
614 1 : if (need_delay && b[0] == 0) {
615 0 : need_delay = false;
616 : }
617 1 : p->set_float((int8_t)b[0], ptype2);
618 1 : b += 1;
619 0 : } else if (ptype == AP_PARAM_INT16) {
620 : int16_t v;
621 0 : memcpy(&v, b, sizeof(v));
622 0 : p->set_float(float(v), ptype2);
623 0 : b += 2;
624 0 : } else if (ptype == AP_PARAM_INT32) {
625 : int32_t v;
626 0 : memcpy(&v, b, sizeof(v));
627 0 : p->set_float(float(v), ptype2);
628 0 : b += 4;
629 0 : } else if (ptype == AP_PARAM_FLOAT) {
630 : float v;
631 0 : memcpy(&v, b, sizeof(v));
632 0 : p->set_float(v, ptype2);
633 0 : b += 4;
634 : }
635 :
636 1 : p->save_sync(false, false);
637 :
638 1 : if (need_delay) {
639 : // let main thread have some time to init backends
640 0 : need_retry = true;
641 0 : hal.scheduler->delay(100);
642 : }
643 : }
644 1 : return true;
645 : }
646 :
647 :
648 : /*
649 : parse incoming parameters
650 : */
651 1 : bool AP_Filesystem_Param::finish_upload(const rfile &r)
652 : {
653 1 : uint8_t loops = 0;
654 1 : while (loops++ < 4) {
655 : bool need_retry;
656 1 : if (!param_upload_parse(r, need_retry)) {
657 0 : return false;
658 : }
659 1 : if (!need_retry) {
660 1 : break;
661 : }
662 : }
663 1 : return true;
664 : }
665 :
666 : #endif // AP_FILESYSTEM_PARAM_ENABLED
|