1/**@file
2
3Copyright (c) 2004 - 2009, Intel Corporation. All rights reserved.<BR>
4This program and the accompanying materials
5are licensed and made available under the terms and conditions of the BSD License
6which accompanies this distribution.  The full text of the license may be found at
7http://opensource.org/licenses/bsd-license.php
8
9THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
10WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
11
12**/
13
14#include "Host.h"
15
16#define EMU_BLOCK_IO_PRIVATE_SIGNATURE SIGNATURE_32 ('E', 'M', 'b', 'k')
17typedef struct {
18  UINTN                       Signature;
19
20  EMU_IO_THUNK_PROTOCOL       *Thunk;
21
22  char                        *Filename;
23  UINTN                       ReadMode;
24  UINTN                       Mode;
25
26  int                         fd;
27
28  BOOLEAN                     RemovableMedia;
29  BOOLEAN                     WriteProtected;
30
31  UINT64                      NumberOfBlocks;
32  UINT32                      BlockSize;
33
34  EMU_BLOCK_IO_PROTOCOL       EmuBlockIo;
35  EFI_BLOCK_IO_MEDIA          *Media;
36
37} EMU_BLOCK_IO_PRIVATE;
38
39#define EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS(a) \
40         CR(a, EMU_BLOCK_IO_PRIVATE, EmuBlockIo, EMU_BLOCK_IO_PRIVATE_SIGNATURE)
41
42
43
44EFI_STATUS
45EmuBlockIoReset (
46  IN EMU_BLOCK_IO_PROTOCOL    *This,
47  IN BOOLEAN                  ExtendedVerification
48  );
49
50
51/*++
52
53This function extends the capability of SetFilePointer to accept 64 bit parameters
54
55**/
56EFI_STATUS
57SetFilePointer64 (
58  IN  EMU_BLOCK_IO_PRIVATE        *Private,
59  IN  INT64                      DistanceToMove,
60  OUT UINT64                     *NewFilePointer,
61  IN  INT32                      MoveMethod
62  )
63{
64  EFI_STATUS    Status;
65  off_t         res;
66  off_t         offset = DistanceToMove;
67
68  Status = EFI_SUCCESS;
69  res = lseek (Private->fd, offset, (int)MoveMethod);
70  if (res == -1) {
71    Status = EFI_INVALID_PARAMETER;
72  }
73
74  if (NewFilePointer != NULL) {
75    *NewFilePointer = res;
76  }
77
78  return Status;
79}
80
81
82EFI_STATUS
83EmuBlockIoOpenDevice (
84  IN EMU_BLOCK_IO_PRIVATE   *Private
85  )
86{
87  EFI_STATUS            Status;
88  UINT64                FileSize;
89  struct statfs         buf;
90
91
92  //
93  // If the device is already opened, close it
94  //
95  if (Private->fd >= 0) {
96    EmuBlockIoReset (&Private->EmuBlockIo, FALSE);
97  }
98
99  //
100  // Open the device
101  //
102  Private->fd = open (Private->Filename, Private->Mode, 0644);
103  if (Private->fd < 0) {
104    printf ("EmuOpenBlock: Could not open %s: %s\n", Private->Filename, strerror(errno));
105    Private->Media->MediaPresent  = FALSE;
106    Status                        = EFI_NO_MEDIA;
107    goto Done;
108  }
109
110  if (!Private->Media->MediaPresent) {
111    //
112    // BugBug: try to emulate if a CD appears - notify drivers to check it out
113    //
114    Private->Media->MediaPresent = TRUE;
115  }
116
117  //
118  // get the size of the file
119  //
120  Status = SetFilePointer64 (Private, 0, &FileSize, SEEK_END);
121  if (EFI_ERROR (Status)) {
122    printf ("EmuOpenBlock: Could not get filesize of %s\n", Private->Filename);
123    Status = EFI_UNSUPPORTED;
124    goto Done;
125  }
126
127  if (FileSize == 0) {
128    // lseek fails on a real device. ioctl calls are OS specific
129#if __APPLE__
130    {
131      UINT32 BlockSize;
132
133      if (ioctl (Private->fd, DKIOCGETBLOCKSIZE, &BlockSize) == 0) {
134        Private->Media->BlockSize = BlockSize;
135      }
136      if (ioctl (Private->fd, DKIOCGETBLOCKCOUNT, &Private->NumberOfBlocks) == 0) {
137        if ((Private->NumberOfBlocks == 0) && (BlockSize == 0x800)) {
138          // A DVD is ~ 4.37 GB so make up a number
139          Private->Media->LastBlock = (0x100000000ULL/0x800) - 1;
140        } else {
141          Private->Media->LastBlock = Private->NumberOfBlocks - 1;
142        }
143      }
144      ioctl (Private->fd, DKIOCGETMAXBLOCKCOUNTWRITE, &Private->Media->OptimalTransferLengthGranularity);
145    }
146#else
147    {
148      size_t BlockSize;
149      UINT64 DiskSize;
150
151      if (ioctl (Private->fd, BLKSSZGET, &BlockSize) == 0) {
152        Private->Media->BlockSize = BlockSize;
153      }
154      if (ioctl (Private->fd, BLKGETSIZE64, &DiskSize) == 0) {
155        Private->NumberOfBlocks = DivU64x32 (DiskSize, (UINT32)BlockSize);
156        Private->Media->LastBlock = Private->NumberOfBlocks - 1;
157      }
158    }
159#endif
160
161  } else {
162    Private->Media->BlockSize = Private->BlockSize;
163    Private->NumberOfBlocks = DivU64x32 (FileSize, Private->Media->BlockSize);
164    Private->Media->LastBlock = Private->NumberOfBlocks - 1;
165
166    if (fstatfs (Private->fd, &buf) == 0) {
167#if __APPLE__
168      Private->Media->OptimalTransferLengthGranularity = buf.f_iosize/buf.f_bsize;
169#else
170      Private->Media->OptimalTransferLengthGranularity = buf.f_bsize/buf.f_bsize;
171#endif
172    }
173  }
174
175  DEBUG ((EFI_D_INIT, "%HEmuOpenBlock: opened %a%N\n", Private->Filename));
176  Status = EFI_SUCCESS;
177
178Done:
179  if (EFI_ERROR (Status)) {
180    if (Private->fd >= 0) {
181      EmuBlockIoReset (&Private->EmuBlockIo, FALSE);
182    }
183  }
184
185  return Status;
186}
187
188
189EFI_STATUS
190EmuBlockIoCreateMapping (
191  IN     EMU_BLOCK_IO_PROTOCOL    *This,
192  IN     EFI_BLOCK_IO_MEDIA       *Media
193  )
194{
195  EFI_STATUS              Status;
196  EMU_BLOCK_IO_PRIVATE    *Private;
197
198  Private = EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS (This);
199
200  Private->Media = Media;
201
202  Media->MediaId          = 0;
203  Media->RemovableMedia   = Private->RemovableMedia;
204  Media->MediaPresent     = TRUE;
205  Media->LogicalPartition = FALSE;
206  Media->ReadOnly         = Private->WriteProtected;
207  Media->WriteCaching     = FALSE;
208  Media->IoAlign          = 1;
209  Media->LastBlock        = 0; // Filled in by OpenDevice
210
211  // EFI_BLOCK_IO_PROTOCOL_REVISION2
212  Media->LowestAlignedLba              = 0;
213  Media->LogicalBlocksPerPhysicalBlock = 0;
214
215
216  // EFI_BLOCK_IO_PROTOCOL_REVISION3
217  Media->OptimalTransferLengthGranularity = 0;
218
219  Status = EmuBlockIoOpenDevice (Private);
220
221
222  return Status;
223}
224
225
226EFI_STATUS
227EmuBlockIoError (
228  IN EMU_BLOCK_IO_PRIVATE      *Private
229  )
230{
231  EFI_STATUS            Status;
232  BOOLEAN               ReinstallBlockIoFlag;
233
234
235  switch (errno) {
236
237  case EAGAIN:
238    Status                        = EFI_NO_MEDIA;
239    Private->Media->ReadOnly      = FALSE;
240    Private->Media->MediaPresent  = FALSE;
241    ReinstallBlockIoFlag          = FALSE;
242    break;
243
244  case EACCES:
245    Private->Media->ReadOnly      = FALSE;
246    Private->Media->MediaPresent  = TRUE;
247    Private->Media->MediaId += 1;
248    ReinstallBlockIoFlag  = TRUE;
249    Status                = EFI_MEDIA_CHANGED;
250    break;
251
252  case EROFS:
253    Private->Media->ReadOnly  = TRUE;
254    ReinstallBlockIoFlag      = FALSE;
255    Status                    = EFI_WRITE_PROTECTED;
256    break;
257
258  default:
259    ReinstallBlockIoFlag  = FALSE;
260    Status                = EFI_DEVICE_ERROR;
261    break;
262  }
263  return Status;
264}
265
266
267EFI_STATUS
268EmuBlockIoReadWriteCommon (
269  IN  EMU_BLOCK_IO_PRIVATE        *Private,
270  IN UINT32                       MediaId,
271  IN EFI_LBA                      Lba,
272  IN UINTN                        BufferSize,
273  IN VOID                         *Buffer,
274  IN CHAR8                        *CallerName
275  )
276{
277  EFI_STATUS  Status;
278  UINTN       BlockSize;
279  UINT64      LastBlock;
280  INT64       DistanceToMove;
281  UINT64      DistanceMoved;
282
283  if (Private->fd < 0) {
284    Status = EmuBlockIoOpenDevice (Private);
285    if (EFI_ERROR (Status)) {
286      return Status;
287    }
288  }
289
290  if (!Private->Media->MediaPresent) {
291    DEBUG ((EFI_D_INIT, "%s: No Media\n", CallerName));
292    return EFI_NO_MEDIA;
293  }
294
295  if (Private->Media->MediaId != MediaId) {
296    return EFI_MEDIA_CHANGED;
297  }
298
299  if ((UINTN) Buffer % Private->Media->IoAlign != 0) {
300    return EFI_INVALID_PARAMETER;
301  }
302
303  //
304  // Verify buffer size
305  //
306  BlockSize = Private->Media->BlockSize;
307  if (BufferSize == 0) {
308    DEBUG ((EFI_D_INIT, "%s: Zero length read\n", CallerName));
309    return EFI_SUCCESS;
310  }
311
312  if ((BufferSize % BlockSize) != 0) {
313    DEBUG ((EFI_D_INIT, "%s: Invalid read size\n", CallerName));
314    return EFI_BAD_BUFFER_SIZE;
315  }
316
317  LastBlock = Lba + (BufferSize / BlockSize) - 1;
318  if (LastBlock > Private->Media->LastBlock) {
319    DEBUG ((EFI_D_INIT, "ReadBlocks: Attempted to read off end of device\n"));
320    return EFI_INVALID_PARAMETER;
321  }
322  //
323  // Seek to End of File
324  //
325  DistanceToMove = MultU64x32 (Lba, BlockSize);
326  Status = SetFilePointer64 (Private, DistanceToMove, &DistanceMoved, SEEK_SET);
327
328  if (EFI_ERROR (Status)) {
329    DEBUG ((EFI_D_INIT, "WriteBlocks: SetFilePointer failed\n"));
330    return EmuBlockIoError (Private);
331  }
332
333  return EFI_SUCCESS;
334}
335
336
337/**
338  Read BufferSize bytes from Lba into Buffer.
339
340  This function reads the requested number of blocks from the device. All the
341  blocks are read, or an error is returned.
342  If EFI_DEVICE_ERROR, EFI_NO_MEDIA,_or EFI_MEDIA_CHANGED is returned and
343  non-blocking I/O is being used, the Event associated with this request will
344  not be signaled.
345
346  @param[in]       This       Indicates a pointer to the calling context.
347  @param[in]       MediaId    Id of the media, changes every time the media is
348                              replaced.
349  @param[in]       Lba        The starting Logical Block Address to read from.
350  @param[in, out]  Token	    A pointer to the token associated with the transaction.
351  @param[in]       BufferSize Size of Buffer, must be a multiple of device block size.
352  @param[out]      Buffer     A pointer to the destination buffer for the data. The
353                              caller is responsible for either having implicit or
354                              explicit ownership of the buffer.
355
356  @retval EFI_SUCCESS           The read request was queued if Token->Event is
357                                not NULL.The data was read correctly from the
358                                device if the Token->Event is NULL.
359  @retval EFI_DEVICE_ERROR      The device reported an error while performing
360                                the read.
361  @retval EFI_NO_MEDIA          There is no media in the device.
362  @retval EFI_MEDIA_CHANGED     The MediaId is not for the current media.
363  @retval EFI_BAD_BUFFER_SIZE   The BufferSize parameter is not a multiple of the
364                                intrinsic block size of the device.
365  @retval EFI_INVALID_PARAMETER The read request contains LBAs that are not valid,
366                                or the buffer is not on proper alignment.
367  @retval EFI_OUT_OF_RESOURCES  The request could not be completed due to a lack
368                                of resources.
369**/
370EFI_STATUS
371EmuBlockIoReadBlocks (
372  IN     EMU_BLOCK_IO_PROTOCOL  *This,
373  IN     UINT32                 MediaId,
374  IN     EFI_LBA                LBA,
375  IN OUT EFI_BLOCK_IO2_TOKEN    *Token,
376  IN     UINTN                  BufferSize,
377     OUT VOID                   *Buffer
378  )
379{
380  EFI_STATUS              Status;
381  EMU_BLOCK_IO_PRIVATE    *Private;
382  ssize_t                 len;
383
384  Private = EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS (This);
385
386  Status  = EmuBlockIoReadWriteCommon (Private, MediaId, LBA, BufferSize, Buffer, "UnixReadBlocks");
387  if (EFI_ERROR (Status)) {
388    goto Done;
389  }
390
391  len = read (Private->fd, Buffer, BufferSize);
392  if (len != BufferSize) {
393    DEBUG ((EFI_D_INIT, "ReadBlocks: ReadFile failed.\n"));
394    Status = EmuBlockIoError (Private);
395    goto Done;
396  }
397
398  //
399  // If we read then media is present.
400  //
401  Private->Media->MediaPresent = TRUE;
402  Status = EFI_SUCCESS;
403
404Done:
405  if (Token != NULL) {
406    if (Token->Event != NULL) {
407      // Caller is responcible for signaling EFI Event
408      Token->TransactionStatus = Status;
409      return EFI_SUCCESS;
410    }
411  }
412  return Status;
413}
414
415
416/**
417  Write BufferSize bytes from Lba into Buffer.
418
419  This function writes the requested number of blocks to the device. All blocks
420  are written, or an error is returned.If EFI_DEVICE_ERROR, EFI_NO_MEDIA,
421  EFI_WRITE_PROTECTED or EFI_MEDIA_CHANGED is returned and non-blocking I/O is
422  being used, the Event associated with this request will not be signaled.
423
424  @param[in]       This       Indicates a pointer to the calling context.
425  @param[in]       MediaId    The media ID that the write request is for.
426  @param[in]       Lba        The starting logical block address to be written. The
427                              caller is responsible for writing to only legitimate
428                              locations.
429  @param[in, out]  Token      A pointer to the token associated with the transaction.
430  @param[in]       BufferSize Size of Buffer, must be a multiple of device block size.
431  @param[in]       Buffer     A pointer to the source buffer for the data.
432
433  @retval EFI_SUCCESS           The write request was queued if Event is not NULL.
434                                The data was written correctly to the device if
435                                the Event is NULL.
436  @retval EFI_WRITE_PROTECTED   The device can not be written to.
437  @retval EFI_NO_MEDIA          There is no media in the device.
438  @retval EFI_MEDIA_CHNAGED     The MediaId does not matched the current device.
439  @retval EFI_DEVICE_ERROR      The device reported an error while performing the write.
440  @retval EFI_BAD_BUFFER_SIZE   The Buffer was not a multiple of the block size of the device.
441  @retval EFI_INVALID_PARAMETER The write request contains LBAs that are not valid,
442                                or the buffer is not on proper alignment.
443  @retval EFI_OUT_OF_RESOURCES  The request could not be completed due to a lack
444                                of resources.
445
446**/
447EFI_STATUS
448EmuBlockIoWriteBlocks (
449  IN     EMU_BLOCK_IO_PROTOCOL  *This,
450  IN     UINT32                 MediaId,
451  IN     EFI_LBA                LBA,
452  IN OUT EFI_BLOCK_IO2_TOKEN    *Token,
453  IN     UINTN                  BufferSize,
454  IN     VOID                   *Buffer
455  )
456{
457  EMU_BLOCK_IO_PRIVATE    *Private;
458  ssize_t                 len;
459  EFI_STATUS              Status;
460
461
462  Private = EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS (This);
463
464  Status  = EmuBlockIoReadWriteCommon (Private, MediaId, LBA, BufferSize, Buffer, "UnixWriteBlocks");
465  if (EFI_ERROR (Status)) {
466    goto Done;
467  }
468
469  len = write (Private->fd, Buffer, BufferSize);
470  if (len != BufferSize) {
471    DEBUG ((EFI_D_INIT, "ReadBlocks: WriteFile failed.\n"));
472    Status = EmuBlockIoError (Private);
473    goto Done;
474  }
475
476  //
477  // If the write succeeded, we are not write protected and media is present.
478  //
479  Private->Media->MediaPresent = TRUE;
480  Private->Media->ReadOnly     = FALSE;
481  Status = EFI_SUCCESS;
482
483Done:
484  if (Token != NULL) {
485    if (Token->Event != NULL) {
486      // Caller is responcible for signaling EFI Event
487      Token->TransactionStatus = Status;
488      return EFI_SUCCESS;
489    }
490  }
491
492  return Status;
493}
494
495
496/**
497  Flush the Block Device.
498
499  If EFI_DEVICE_ERROR, EFI_NO_MEDIA,_EFI_WRITE_PROTECTED or EFI_MEDIA_CHANGED
500  is returned and non-blocking I/O is being used, the Event associated with
501  this request will not be signaled.
502
503  @param[in]      This     Indicates a pointer to the calling context.
504  @param[in,out]  Token    A pointer to the token associated with the transaction
505
506  @retval EFI_SUCCESS          The flush request was queued if Event is not NULL.
507                               All outstanding data was written correctly to the
508                               device if the Event is NULL.
509  @retval EFI_DEVICE_ERROR     The device reported an error while writting back
510                               the data.
511  @retval EFI_WRITE_PROTECTED  The device cannot be written to.
512  @retval EFI_NO_MEDIA         There is no media in the device.
513  @retval EFI_MEDIA_CHANGED    The MediaId is not for the current media.
514  @retval EFI_OUT_OF_RESOURCES The request could not be completed due to a lack
515                               of resources.
516
517**/
518EFI_STATUS
519EmuBlockIoFlushBlocks (
520  IN     EMU_BLOCK_IO_PROTOCOL    *This,
521  IN OUT EFI_BLOCK_IO2_TOKEN      *Token
522  )
523{
524  EMU_BLOCK_IO_PRIVATE *Private;
525
526  Private = EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS (This);
527
528  if (Private->fd >= 0) {
529    fsync (Private->fd);
530#if __APPLE__
531    fcntl (Private->fd, F_FULLFSYNC);
532#endif
533  }
534
535
536  if (Token != NULL) {
537    if (Token->Event != NULL) {
538      // Caller is responcible for signaling EFI Event
539      Token->TransactionStatus = EFI_SUCCESS;
540      return EFI_SUCCESS;
541    }
542  }
543
544  return EFI_SUCCESS;
545}
546
547
548/**
549  Reset the block device hardware.
550
551  @param[in]  This                 Indicates a pointer to the calling context.
552  @param[in]  ExtendedVerification Indicates that the driver may perform a more
553                                   exhausive verfication operation of the device
554                                   during reset.
555
556  @retval EFI_SUCCESS          The device was reset.
557  @retval EFI_DEVICE_ERROR     The device is not functioning properly and could
558                               not be reset.
559
560**/
561EFI_STATUS
562EmuBlockIoReset (
563  IN EMU_BLOCK_IO_PROTOCOL    *This,
564  IN BOOLEAN                  ExtendedVerification
565  )
566{
567  EMU_BLOCK_IO_PRIVATE *Private;
568
569  Private = EMU_BLOCK_IO_PRIVATE_DATA_FROM_THIS (This);
570
571  if (Private->fd >= 0) {
572    close (Private->fd);
573    Private->fd = -1;
574  }
575
576  return EFI_SUCCESS;
577}
578
579
580char *
581StdDupUnicodeToAscii (
582  IN  CHAR16 *Str
583  )
584{
585  UINTN   Size;
586  char    *Ascii;
587  char    *Ptr;
588
589  Size = StrLen (Str) + 1;
590  Ascii = malloc (Size);
591  if (Ascii == NULL) {
592    return NULL;
593  }
594
595  for (Ptr = Ascii; *Str != '\0'; Ptr++, Str++) {
596    *Ptr = *Str;
597  }
598  *Ptr = 0;
599
600  return Ascii;
601}
602
603
604EMU_BLOCK_IO_PROTOCOL gEmuBlockIoProtocol = {
605  GasketEmuBlockIoReset,
606  GasketEmuBlockIoReadBlocks,
607  GasketEmuBlockIoWriteBlocks,
608  GasketEmuBlockIoFlushBlocks,
609  GasketEmuBlockIoCreateMapping
610};
611
612EFI_STATUS
613EmuBlockIoThunkOpen (
614  IN  EMU_IO_THUNK_PROTOCOL   *This
615  )
616{
617  EMU_BLOCK_IO_PRIVATE  *Private;
618  char                  *Str;
619
620  if (This->Private != NULL) {
621    return EFI_ALREADY_STARTED;
622  }
623
624  if (!CompareGuid (This->Protocol, &gEmuBlockIoProtocolGuid)) {
625    return EFI_UNSUPPORTED;
626  }
627
628  Private = malloc (sizeof (EMU_BLOCK_IO_PRIVATE));
629  if (Private == NULL) {
630    return EFI_OUT_OF_RESOURCES;
631  }
632
633
634  Private->Signature = EMU_BLOCK_IO_PRIVATE_SIGNATURE;
635  Private->Thunk     = This;
636  CopyMem (&Private->EmuBlockIo, &gEmuBlockIoProtocol, sizeof (gEmuBlockIoProtocol));
637  Private->fd        = -1;
638  Private->BlockSize = 512;
639
640  Private->Filename = StdDupUnicodeToAscii (This->ConfigString);
641  if (Private->Filename == NULL) {
642    return EFI_OUT_OF_RESOURCES;
643  }
644
645  Str = strstr (Private->Filename, ":");
646  if (Str == NULL) {
647    Private->RemovableMedia = FALSE;
648    Private->WriteProtected = FALSE;
649  } else {
650    for (*Str++ = '\0'; *Str != 0; Str++) {
651      if (*Str == 'R' || *Str == 'F') {
652        Private->RemovableMedia = (BOOLEAN) (*Str == 'R');
653      }
654      if (*Str == 'O' || *Str == 'W') {
655        Private->WriteProtected  = (BOOLEAN) (*Str == 'O');
656      }
657      if (*Str == ':') {
658        Private->BlockSize = strtol (++Str, NULL, 0);
659        break;
660      }
661    }
662  }
663
664  Private->Mode = Private->WriteProtected ? O_RDONLY : O_RDWR;
665
666  This->Interface = &Private->EmuBlockIo;
667  This->Private   = Private;
668  return EFI_SUCCESS;
669}
670
671
672EFI_STATUS
673EmuBlockIoThunkClose (
674  IN  EMU_IO_THUNK_PROTOCOL   *This
675  )
676{
677  EMU_BLOCK_IO_PRIVATE  *Private;
678
679  if (!CompareGuid (This->Protocol, &gEmuBlockIoProtocolGuid)) {
680    return EFI_UNSUPPORTED;
681  }
682
683  Private = This->Private;
684
685  if (This->Private != NULL) {
686    if (Private->Filename != NULL) {
687      free (Private->Filename);
688    }
689    free (This->Private);
690    This->Private = NULL;
691  }
692
693  return EFI_SUCCESS;
694}
695
696
697
698EMU_IO_THUNK_PROTOCOL gBlockIoThunkIo = {
699  &gEmuBlockIoProtocolGuid,
700  NULL,
701  NULL,
702  0,
703  GasketBlockIoThunkOpen,
704  GasketBlockIoThunkClose,
705  NULL
706};
707
708
709