FFmpeg
 All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
ftp.c
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2013 Lukasz Marek <lukasz.m.luki@gmail.com>
3  *
4  * This file is part of FFmpeg.
5  *
6  * FFmpeg is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * FFmpeg is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with FFmpeg; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19  */
20 
21 #include "libavutil/avstring.h"
22 #include "avformat.h"
23 #include "internal.h"
24 #include "url.h"
25 #include "libavutil/opt.h"
26 #include "libavutil/bprint.h"
27 
28 #define CONTROL_BUFFER_SIZE 1024
29 #define CREDENTIALS_BUFFER_SIZE 128
30 
31 typedef enum {
37 } FTPState;
38 
39 typedef struct {
40  const AVClass *class;
41  URLContext *conn_control; /**< Control connection */
42  URLContext *conn_data; /**< Data connection, NULL when not connected */
43  uint8_t control_buffer[CONTROL_BUFFER_SIZE]; /**< Control connection buffer */
44  uint8_t *control_buf_ptr, *control_buf_end;
45  int server_data_port; /**< Data connection port opened by server, -1 on error. */
46  int server_control_port; /**< Control connection port, default is 21 */
47  char hostname[512]; /**< Server address. */
48  char credencials[CREDENTIALS_BUFFER_SIZE]; /**< Authentication data */
49  char path[MAX_URL_SIZE]; /**< Path to resource on server. */
50  int64_t filesize; /**< Size of file on server, -1 on error. */
51  int64_t position; /**< Current position, calculated. */
52  int rw_timeout; /**< Network timeout. */
53  const char *anonymous_password; /**< Password to be used for anonymous user. An email should be used. */
54  int write_seekable; /**< Control seekability, 0 = disable, 1 = enable. */
55  FTPState state; /**< State of data connection */
56 } FTPContext;
57 
58 #define OFFSET(x) offsetof(FTPContext, x)
59 #define D AV_OPT_FLAG_DECODING_PARAM
60 #define E AV_OPT_FLAG_ENCODING_PARAM
61 static const AVOption options[] = {
62  {"timeout", "set timeout of socket I/O operations", OFFSET(rw_timeout), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, D|E },
63  {"ftp-write-seekable", "control seekability of connection during encoding", OFFSET(write_seekable), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 1, E },
64  {"ftp-anonymous-password", "password for anonymous login. E-mail address should be used.", OFFSET(anonymous_password), AV_OPT_TYPE_STRING, { 0 }, 0, 0, D|E },
65  {NULL}
66 };
67 
68 static const AVClass ftp_context_class = {
69  .class_name = "ftp",
70  .item_name = av_default_item_name,
71  .option = options,
72  .version = LIBAVUTIL_VERSION_INT,
73 };
74 
75 static int ftp_getc(FTPContext *s)
76 {
77  int len;
78  if (s->control_buf_ptr >= s->control_buf_end) {
80  if (len < 0) {
81  return len;
82  } else if (!len) {
83  return -1;
84  } else {
87  }
88  }
89  return *s->control_buf_ptr++;
90 }
91 
92 static int ftp_get_line(FTPContext *s, char *line, int line_size)
93 {
94  int ch;
95  char *q = line;
96 
97  for (;;) {
98  ch = ftp_getc(s);
99  if (ch < 0) {
100  return ch;
101  }
102  if (ch == '\n') {
103  /* process line */
104  if (q > line && q[-1] == '\r')
105  q--;
106  *q = '\0';
107  return 0;
108  } else {
109  if ((q - line) < line_size - 1)
110  *q++ = ch;
111  }
112  }
113 }
114 
115 /*
116  * This routine returns ftp server response code.
117  * Server may send more than one response for a certain command.
118  * First expected code is returned.
119  */
120 static int ftp_status(FTPContext *s, char **line, const int response_codes[])
121 {
122  int err, i, dash = 0, result = 0, code_found = 0, linesize;
123  char buf[CONTROL_BUFFER_SIZE];
124  AVBPrint line_buffer;
125 
126  if (line)
127  av_bprint_init(&line_buffer, 0, AV_BPRINT_SIZE_AUTOMATIC);
128 
129  while (!code_found || dash) {
130  if ((err = ftp_get_line(s, buf, sizeof(buf))) < 0) {
131  if (line)
132  av_bprint_finalize(&line_buffer, NULL);
133  return err;
134  }
135 
136  av_log(s, AV_LOG_DEBUG, "%s\n", buf);
137 
138  linesize = strlen(buf);
139  err = 0;
140  if (linesize >= 3) {
141  for (i = 0; i < 3; ++i) {
142  if (buf[i] < '0' || buf[i] > '9') {
143  err = 0;
144  break;
145  }
146  err *= 10;
147  err += buf[i] - '0';
148  }
149  }
150 
151  if (!code_found) {
152  if (err >= 500) {
153  code_found = 1;
154  result = err;
155  } else
156  for (i = 0; response_codes[i]; ++i) {
157  if (err == response_codes[i]) {
158  code_found = 1;
159  result = err;
160  break;
161  }
162  }
163  }
164  if (code_found) {
165  if (line)
166  av_bprintf(&line_buffer, "%s\r\n", buf);
167  if (linesize >= 4) {
168  if (!dash && buf[3] == '-')
169  dash = err;
170  else if (err == dash && buf[3] == ' ')
171  dash = 0;
172  }
173  }
174  }
175 
176  if (line)
177  av_bprint_finalize(&line_buffer, line);
178  return result;
179 }
180 
181 static int ftp_send_command(FTPContext *s, const char *command,
182  const int response_codes[], char **response)
183 {
184  int err;
185 
186  if ((err = ffurl_write(s->conn_control, command, strlen(command))) < 0)
187  return err;
188  if (!err)
189  return -1;
190 
191  /* return status */
192  if (response_codes) {
193  return ftp_status(s, response, response_codes);
194  }
195  return 0;
196 }
197 
199 {
200  ffurl_closep(&s->conn_data);
201  s->position = 0;
202  s->state = DISCONNECTED;
203 }
204 
206 {
209 }
210 
211 static int ftp_auth(FTPContext *s)
212 {
213  const char *user = NULL, *pass = NULL;
214  char *end = NULL, buf[CONTROL_BUFFER_SIZE], credencials[CREDENTIALS_BUFFER_SIZE];
215  int err;
216  static const int user_codes[] = {331, 230, 0};
217  static const int pass_codes[] = {230, 0};
218 
219  /* Authentication may be repeated, original string has to be saved */
220  av_strlcpy(credencials, s->credencials, sizeof(credencials));
221 
222  user = av_strtok(credencials, ":", &end);
223  pass = av_strtok(end, ":", &end);
224 
225  if (!user) {
226  user = "anonymous";
227  pass = s->anonymous_password ? s->anonymous_password : "nopassword";
228  }
229 
230  snprintf(buf, sizeof(buf), "USER %s\r\n", user);
231  err = ftp_send_command(s, buf, user_codes, NULL);
232  if (err == 331) {
233  if (pass) {
234  snprintf(buf, sizeof(buf), "PASS %s\r\n", pass);
235  err = ftp_send_command(s, buf, pass_codes, NULL);
236  } else
237  return AVERROR(EACCES);
238  }
239  if (err != 230)
240  return AVERROR(EACCES);
241 
242  return 0;
243 }
244 
246 {
247  char *res = NULL, *start = NULL, *end = NULL;
248  int i;
249  static const char d = '|';
250  static const char *command = "EPSV\r\n";
251  static const int epsv_codes[] = {229, 0};
252 
253  if (ftp_send_command(s, command, epsv_codes, &res) != 229 || !res)
254  goto fail;
255 
256  for (i = 0; res[i]; ++i) {
257  if (res[i] == '(') {
258  start = res + i + 1;
259  } else if (res[i] == ')') {
260  end = res + i;
261  break;
262  }
263  }
264  if (!start || !end)
265  goto fail;
266 
267  *end = '\0';
268  if (strlen(start) < 5)
269  goto fail;
270  if (start[0] != d || start[1] != d || start[2] != d || end[-1] != d)
271  goto fail;
272  start += 3;
273  end[-1] = '\0';
274 
275  s->server_data_port = atoi(start);
276  av_dlog(s, "Server data port: %d\n", s->server_data_port);
277 
278  av_free(res);
279  return 0;
280 
281  fail:
282  av_free(res);
283  s->server_data_port = -1;
284  return AVERROR(ENOSYS);
285 }
286 
288 {
289  char *res = NULL, *start = NULL, *end = NULL;
290  int i;
291  static const char *command = "PASV\r\n";
292  static const int pasv_codes[] = {227, 0};
293 
294  if (ftp_send_command(s, command, pasv_codes, &res) != 227 || !res)
295  goto fail;
296 
297  for (i = 0; res[i]; ++i) {
298  if (res[i] == '(') {
299  start = res + i + 1;
300  } else if (res[i] == ')') {
301  end = res + i;
302  break;
303  }
304  }
305  if (!start || !end)
306  goto fail;
307 
308  *end = '\0';
309  /* skip ip */
310  if (!av_strtok(start, ",", &end)) goto fail;
311  if (!av_strtok(end, ",", &end)) goto fail;
312  if (!av_strtok(end, ",", &end)) goto fail;
313  if (!av_strtok(end, ",", &end)) goto fail;
314 
315  /* parse port number */
316  start = av_strtok(end, ",", &end);
317  if (!start) goto fail;
318  s->server_data_port = atoi(start) * 256;
319  start = av_strtok(end, ",", &end);
320  if (!start) goto fail;
321  s->server_data_port += atoi(start);
322  av_dlog(s, "Server data port: %d\n", s->server_data_port);
323 
324  av_free(res);
325  return 0;
326 
327  fail:
328  av_free(res);
329  s->server_data_port = -1;
330  return AVERROR(EIO);
331 }
332 
334 {
335  char *res = NULL, *start = NULL, *end = NULL;
336  int i;
337  static const char *command = "PWD\r\n";
338  static const int pwd_codes[] = {257, 0};
339 
340  if (ftp_send_command(s, command, pwd_codes, &res) != 257 || !res)
341  goto fail;
342 
343  for (i = 0; res[i]; ++i) {
344  if (res[i] == '"') {
345  if (!start) {
346  start = res + i + 1;
347  continue;
348  }
349  end = res + i;
350  break;
351  }
352  }
353 
354  if (!end)
355  goto fail;
356 
357  if (end > res && end[-1] == '/') {
358  end[-1] = '\0';
359  } else
360  *end = '\0';
361  av_strlcpy(s->path, start, sizeof(s->path));
362 
363  av_free(res);
364  return 0;
365 
366  fail:
367  av_free(res);
368  return AVERROR(EIO);
369 }
370 
372 {
374  char *res = NULL;
375  static const int size_codes[] = {213, 0};
376 
377  snprintf(command, sizeof(command), "SIZE %s\r\n", s->path);
378  if (ftp_send_command(s, command, size_codes, &res) == 213 && res) {
379  s->filesize = strtoll(&res[4], NULL, 10);
380  } else {
381  s->filesize = -1;
382  av_free(res);
383  return AVERROR(EIO);
384  }
385 
386  av_free(res);
387  return 0;
388 }
389 
391 {
393  static const int retr_codes[] = {150, 0};
394 
395  snprintf(command, sizeof(command), "RETR %s\r\n", s->path);
396  if (ftp_send_command(s, command, retr_codes, NULL) != 150)
397  return AVERROR(EIO);
398 
399  s->state = DOWNLOADING;
400 
401  return 0;
402 }
403 
404 static int ftp_store(FTPContext *s)
405 {
407  static const int stor_codes[] = {150, 0};
408 
409  snprintf(command, sizeof(command), "STOR %s\r\n", s->path);
410  if (ftp_send_command(s, command, stor_codes, NULL) != 150)
411  return AVERROR(EIO);
412 
413  s->state = UPLOADING;
414 
415  return 0;
416 }
417 
418 static int ftp_type(FTPContext *s)
419 {
420  static const char *command = "TYPE I\r\n";
421  static const int type_codes[] = {200, 0};
422 
423  if (ftp_send_command(s, command, type_codes, NULL) != 200)
424  return AVERROR(EIO);
425 
426  return 0;
427 }
428 
429 static int ftp_restart(FTPContext *s, int64_t pos)
430 {
432  static const int rest_codes[] = {350, 0};
433 
434  snprintf(command, sizeof(command), "REST %"PRId64"\r\n", pos);
435  if (ftp_send_command(s, command, rest_codes, NULL) != 350)
436  return AVERROR(EIO);
437 
438  return 0;
439 }
440 
442 {
443  static const char *feat_command = "FEAT\r\n";
444  static const char *enable_utf8_command = "OPTS UTF8 ON\r\n";
445  static const int feat_codes[] = {211, 0};
446  static const int opts_codes[] = {200, 451};
447  char *feat;
448 
449  if (ftp_send_command(s, feat_command, feat_codes, &feat) == 211) {
450  if (av_stristr(feat, "UTF8"))
451  ftp_send_command(s, enable_utf8_command, opts_codes, NULL);
452  }
453  return 0;
454 }
455 
457 {
458  char buf[CONTROL_BUFFER_SIZE], opts_format[20], *response = NULL;
459  int err;
460  AVDictionary *opts = NULL;
461  FTPContext *s = h->priv_data;
462  static const int connect_codes[] = {220, 0};
463 
464  if (!s->conn_control) {
465  ff_url_join(buf, sizeof(buf), "tcp", NULL,
466  s->hostname, s->server_control_port, NULL);
467  if (s->rw_timeout != -1) {
468  snprintf(opts_format, sizeof(opts_format), "%d", s->rw_timeout);
469  av_dict_set(&opts, "timeout", opts_format, 0);
470  } /* if option is not given, don't pass it and let tcp use its own default */
472  &h->interrupt_callback, &opts);
473  av_dict_free(&opts);
474  if (err < 0) {
475  av_log(h, AV_LOG_ERROR, "Cannot open control connection\n");
476  return err;
477  }
478 
479  /* check if server is ready */
480  if (ftp_status(s, ((h->flags & AVIO_FLAG_WRITE) ? &response : NULL), connect_codes) != 220) {
481  av_log(h, AV_LOG_ERROR, "FTP server not ready for new users\n");
482  return AVERROR(EACCES);
483  }
484 
485  if ((h->flags & AVIO_FLAG_WRITE) && av_stristr(response, "pure-ftpd")) {
486  av_log(h, AV_LOG_WARNING, "Pure-FTPd server is used as an output protocol. It is known issue this implementation may produce incorrect content and it cannot be fixed at this moment.");
487  }
488  av_free(response);
489 
490  if ((err = ftp_auth(s)) < 0) {
491  av_log(h, AV_LOG_ERROR, "FTP authentication failed\n");
492  return err;
493  }
494 
495  if ((err = ftp_type(s)) < 0) {
496  av_log(h, AV_LOG_ERROR, "Set content type failed\n");
497  return err;
498  }
499 
500  ftp_features(s);
501  }
502  return 0;
503 }
504 
506 {
507  int err;
508  char buf[CONTROL_BUFFER_SIZE], opts_format[20];
509  AVDictionary *opts = NULL;
510  FTPContext *s = h->priv_data;
511 
512  if (!s->conn_data) {
513  /* Enter passive mode */
514  if (ftp_passive_mode_epsv(s) < 0) {
515  /* Use PASV as fallback */
516  if ((err = ftp_passive_mode(s)) < 0)
517  return err;
518  }
519  /* Open data connection */
520  ff_url_join(buf, sizeof(buf), "tcp", NULL, s->hostname, s->server_data_port, NULL);
521  if (s->rw_timeout != -1) {
522  snprintf(opts_format, sizeof(opts_format), "%d", s->rw_timeout);
523  av_dict_set(&opts, "timeout", opts_format, 0);
524  } /* if option is not given, don't pass it and let tcp use its own default */
525  err = ffurl_open(&s->conn_data, buf, h->flags,
526  &h->interrupt_callback, &opts);
527  av_dict_free(&opts);
528  if (err < 0)
529  return err;
530 
531  if (s->position)
532  if ((err = ftp_restart(s, s->position)) < 0)
533  return err;
534  }
535  s->state = READY;
536  return 0;
537 }
538 
539 static int ftp_abort(URLContext *h)
540 {
541  static const char *command = "ABOR\r\n";
542  int err;
543  static const int abor_codes[] = {225, 226, 0};
544  FTPContext *s = h->priv_data;
545 
546  /* According to RCF 959:
547  "ABOR command tells the server to abort the previous FTP
548  service command and any associated transfer of data."
549 
550  There are FTP server implementations that don't response
551  to any commands during data transfer in passive mode (including ABOR).
552 
553  This implementation closes data connection by force.
554  */
555 
556  if (ftp_send_command(s, command, NULL, NULL) < 0) {
558  if ((err = ftp_connect_control_connection(h)) < 0) {
559  av_log(h, AV_LOG_ERROR, "Reconnect failed.\n");
560  return err;
561  }
562  } else {
564  if (ftp_status(s, NULL, abor_codes) < 225) {
565  /* wu-ftpd also closes control connection after data connection closing */
567  if ((err = ftp_connect_control_connection(h)) < 0) {
568  av_log(h, AV_LOG_ERROR, "Reconnect failed.\n");
569  return err;
570  }
571  }
572  }
573 
574  return 0;
575 }
576 
577 static int ftp_open(URLContext *h, const char *url, int flags)
578 {
579  char proto[10], path[MAX_URL_SIZE];
580  int err;
581  FTPContext *s = h->priv_data;
582 
583  av_dlog(h, "ftp protocol open\n");
584 
585  s->state = DISCONNECTED;
586  s->filesize = -1;
587  s->position = 0;
588 
589  av_url_split(proto, sizeof(proto),
590  s->credencials, sizeof(s->credencials),
591  s->hostname, sizeof(s->hostname),
593  path, sizeof(path),
594  url);
595 
596  if (s->server_control_port < 0 || s->server_control_port > 65535)
597  s->server_control_port = 21;
598 
599  if ((err = ftp_connect_control_connection(h)) < 0)
600  goto fail;
601 
602  if ((err = ftp_current_dir(s)) < 0)
603  goto fail;
604  av_strlcat(s->path, path, sizeof(s->path));
605 
606  if (ftp_restart(s, 0) < 0) {
607  h->is_streamed = 1;
608  } else {
609  if (ftp_file_size(s) < 0 && flags & AVIO_FLAG_READ)
610  h->is_streamed = 1;
611  if (s->write_seekable != 1 && flags & AVIO_FLAG_WRITE)
612  h->is_streamed = 1;
613  }
614 
615  return 0;
616 
617  fail:
618  av_log(h, AV_LOG_ERROR, "FTP open failed\n");
620  ffurl_closep(&s->conn_data);
621  return err;
622 }
623 
624 static int64_t ftp_seek(URLContext *h, int64_t pos, int whence)
625 {
626  FTPContext *s = h->priv_data;
627  int err;
628  int64_t new_pos, fake_pos;
629 
630  av_dlog(h, "ftp protocol seek %"PRId64" %d\n", pos, whence);
631 
632  switch(whence) {
633  case AVSEEK_SIZE:
634  return s->filesize;
635  case SEEK_SET:
636  new_pos = pos;
637  break;
638  case SEEK_CUR:
639  new_pos = s->position + pos;
640  break;
641  case SEEK_END:
642  if (s->filesize < 0)
643  return AVERROR(EIO);
644  new_pos = s->filesize + pos;
645  break;
646  default:
647  return AVERROR(EINVAL);
648  }
649 
650  if (h->is_streamed)
651  return AVERROR(EIO);
652 
653  if (new_pos < 0) {
654  av_log(h, AV_LOG_ERROR, "Seeking to nagative position.\n");
655  return AVERROR(EINVAL);
656  }
657 
658  fake_pos = s->filesize != -1 ? FFMIN(new_pos, s->filesize) : new_pos;
659  if (fake_pos != s->position) {
660  if ((err = ftp_abort(h)) < 0)
661  return err;
662  s->position = fake_pos;
663  }
664  return new_pos;
665 }
666 
667 static int ftp_read(URLContext *h, unsigned char *buf, int size)
668 {
669  FTPContext *s = h->priv_data;
670  int read, err, retry_done = 0;
671 
672  av_dlog(h, "ftp protocol read %d bytes\n", size);
673  retry:
674  if (s->state == DISCONNECTED) {
675  /* optimization */
676  if (s->position >= s->filesize)
677  return 0;
678  if ((err = ftp_connect_data_connection(h)) < 0)
679  return err;
680  }
681  if (s->state == READY) {
682  if (s->position >= s->filesize)
683  return 0;
684  if ((err = ftp_retrieve(s)) < 0)
685  return err;
686  }
687  if (s->conn_data && s->state == DOWNLOADING) {
688  read = ffurl_read(s->conn_data, buf, size);
689  if (read >= 0) {
690  s->position += read;
691  if (s->position >= s->filesize) {
692  /* server will terminate, but keep current position to avoid madness */
693  /* save position to restart from it */
694  int64_t pos = s->position;
695  if (ftp_abort(h) < 0) {
696  s->position = pos;
697  return AVERROR(EIO);
698  }
699  s->position = pos;
700  }
701  }
702  if (read <= 0 && s->position < s->filesize && !h->is_streamed) {
703  /* Server closed connection. Probably due to inactivity */
704  int64_t pos = s->position;
705  av_log(h, AV_LOG_INFO, "Reconnect to FTP server.\n");
706  if ((err = ftp_abort(h)) < 0)
707  return err;
708  if ((err = ftp_seek(h, pos, SEEK_SET)) < 0) {
709  av_log(h, AV_LOG_ERROR, "Position cannot be restored.\n");
710  return err;
711  }
712  if (!retry_done) {
713  retry_done = 1;
714  goto retry;
715  }
716  }
717  return read;
718  }
719 
720  av_log(h, AV_LOG_DEBUG, "FTP read failed\n");
721  return AVERROR(EIO);
722 }
723 
724 static int ftp_write(URLContext *h, const unsigned char *buf, int size)
725 {
726  int err;
727  FTPContext *s = h->priv_data;
728  int written;
729 
730  av_dlog(h, "ftp protocol write %d bytes\n", size);
731 
732  if (s->state == DISCONNECTED) {
733  if ((err = ftp_connect_data_connection(h)) < 0)
734  return err;
735  }
736  if (s->state == READY) {
737  if ((err = ftp_store(s)) < 0)
738  return err;
739  }
740  if (s->conn_data && s->state == UPLOADING) {
741  written = ffurl_write(s->conn_data, buf, size);
742  if (written > 0) {
743  s->position += written;
744  s->filesize = FFMAX(s->filesize, s->position);
745  }
746  return written;
747  }
748 
749  av_log(h, AV_LOG_ERROR, "FTP write failed\n");
750  return AVERROR(EIO);
751 }
752 
753 static int ftp_close(URLContext *h)
754 {
755  av_dlog(h, "ftp protocol close\n");
756 
758 
759  return 0;
760 }
761 
763 {
764  FTPContext *s = h->priv_data;
765 
766  av_dlog(h, "ftp protocol get_file_handle\n");
767 
768  if (s->conn_data)
769  return ffurl_get_file_handle(s->conn_data);
770 
771  return AVERROR(EIO);
772 }
773 
774 static int ftp_shutdown(URLContext *h, int flags)
775 {
776  FTPContext *s = h->priv_data;
777 
778  av_dlog(h, "ftp protocol shutdown\n");
779 
780  if (s->conn_data)
781  return ffurl_shutdown(s->conn_data, flags);
782 
783  return AVERROR(EIO);
784 }
785 
787  .name = "ftp",
788  .url_open = ftp_open,
789  .url_read = ftp_read,
790  .url_write = ftp_write,
791  .url_seek = ftp_seek,
792  .url_close = ftp_close,
793  .url_get_file_handle = ftp_get_file_handle,
794  .url_shutdown = ftp_shutdown,
795  .priv_data_size = sizeof(FTPContext),
796  .priv_data_class = &ftp_context_class,
798 };