diff options
author | Corinna Vinschen <corinna@vinschen.de> | 2022-08-04 17:58:50 +0300 |
---|---|---|
committer | Corinna Vinschen <corinna@vinschen.de> | 2022-08-05 13:02:11 +0300 |
commit | 007e23d6390af11582e55453269b7a51c723d2dd (patch) | |
tree | 8e8cff3ca23f5e56d9766a5ee6c6abb366611b07 /winsup/cygwin/fhandler/floppy.cc | |
parent | 1e428bee1c5ef7c76ba4e46e6693b913edc9bbf3 (diff) |
Cygwin: Reorganize cygwin source dir
Create subdirs and move files accordingly:
- DevDocs: doc files
- fhandler: fhandler sources, split fhandler.cc into base.cc and null.cc
- local_includes: local include files
- scripts: scripts called during build
- sec: security sources
Signed-off-by: Corinna Vinschen <corinna@vinschen.de>
Diffstat (limited to 'winsup/cygwin/fhandler/floppy.cc')
-rw-r--r-- | winsup/cygwin/fhandler/floppy.cc | 766 |
1 files changed, 766 insertions, 0 deletions
diff --git a/winsup/cygwin/fhandler/floppy.cc b/winsup/cygwin/fhandler/floppy.cc new file mode 100644 index 000000000..e883ab697 --- /dev/null +++ b/winsup/cygwin/fhandler/floppy.cc @@ -0,0 +1,766 @@ +/* fhandler_floppy.cc. See fhandler.h for a description of the + fhandler classes. + +This file is part of Cygwin. + +This software is a copyrighted work licensed under the terms of the +Cygwin license. Please consult the file "CYGWIN_LICENSE" for +details. */ + +#include "winsup.h" +#include <alloca.h> +#include <unistd.h> +#include <sys/param.h> +#include <winioctl.h> +#include <cygwin/rdevio.h> +#include <cygwin/hdreg.h> +#include <cygwin/fs.h> +#include "cygerrno.h" +#include "security.h" +#include "path.h" +#include "fhandler.h" +#include "ntdll.h" + +#define IS_EOM(err) ((err) == ERROR_INVALID_PARAMETER \ + || (err) == ERROR_SEEK \ + || (err) == ERROR_SECTOR_NOT_FOUND) + +#define bytes_per_sector devbufalign + +/**********************************************************************/ +/* fhandler_dev_floppy */ + +fhandler_dev_floppy::fhandler_dev_floppy () + : fhandler_dev_raw (), status () +{ +} + +int +fhandler_dev_floppy::get_drive_info (struct hd_geometry *geo) +{ + char dbuf[256]; + char pbuf[256]; + + DISK_GEOMETRY_EX *dix = NULL; + DISK_GEOMETRY *di = NULL; + PARTITION_INFORMATION_EX *pix = NULL; + PARTITION_INFORMATION *pi = NULL; + DWORD bytes_read = 0; + + /* Always try using the new EX ioctls first. If not available, fall back + to trying the non-EX ioctls which are unfortunately not implemented in + the floppy driver. */ + if (get_major () != DEV_FLOPPY_MAJOR) + { + if (!DeviceIoControl (get_handle (), + IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, NULL, 0, + dbuf, 256, &bytes_read, NULL)) + __seterrno (); + else + { + dix = (DISK_GEOMETRY_EX *) dbuf; + di = &dix->Geometry; + } + } + if (!di) + { + if (!DeviceIoControl (get_handle (), + IOCTL_DISK_GET_DRIVE_GEOMETRY, NULL, 0, + dbuf, 256, &bytes_read, NULL)) + { + __seterrno (); + return -1; + } + di = (DISK_GEOMETRY *) dbuf; + } + if (dix) /* Don't try IOCTL_DISK_GET_PARTITION_INFO_EX if + IOCTL_DISK_GET_DRIVE_GEOMETRY_EX didn't work. + Probably a floppy.*/ + { + if (!DeviceIoControl (get_handle (), + IOCTL_DISK_GET_PARTITION_INFO_EX, NULL, 0, + pbuf, 256, &bytes_read, NULL)) + __seterrno (); + else + pix = (PARTITION_INFORMATION_EX *) pbuf; + } + if (!pix && get_major () != DEV_FLOPPY_MAJOR) + { + /* It's unlikely that this code path will be used at all. Either the + _EX call already worked, or it's a floppy. But it doesn't hurt to + keep the code in. */ + if (!DeviceIoControl (get_handle (), + IOCTL_DISK_GET_PARTITION_INFO, NULL, 0, + pbuf, 256, &bytes_read, NULL)) + __seterrno (); + else + pi = (PARTITION_INFORMATION *) pbuf; + } + debug_printf ("disk geometry: (%D cyl)*(%u trk)*(%u sec)*(%u bps)", + di->Cylinders.QuadPart, + di->TracksPerCylinder, + di->SectorsPerTrack, + di->BytesPerSector); + bytes_per_sector = di->BytesPerSector; + if (pix) + { + debug_printf ("partition info: offset %D length %D", + pix->StartingOffset.QuadPart, + pix->PartitionLength.QuadPart); + drive_size = pix->PartitionLength.QuadPart; + } + else if (pi) + { + debug_printf ("partition info: offset %D length %D", + pi->StartingOffset.QuadPart, + pi->PartitionLength.QuadPart); + drive_size = pi->PartitionLength.QuadPart; + } + else /* Floppy drive. */ + drive_size = di->Cylinders.QuadPart * di->TracksPerCylinder + * di->SectorsPerTrack * di->BytesPerSector; + if (geo) + { + geo->heads = di->TracksPerCylinder; + geo->sectors = di->SectorsPerTrack; + geo->cylinders = di->Cylinders.LowPart; + if (pix) + geo->start = pix->StartingOffset.QuadPart >> 9ULL; + else if (pi) + geo->start = pi->StartingOffset.QuadPart >> 9ULL; + else + geo->start = 0; + } + debug_printf ("drive size: %D", drive_size); + + return 0; +} + +/* Wrapper functions for ReadFile and WriteFile to simplify error handling. */ +BOOL +fhandler_dev_floppy::read_file (void *buf, DWORD to_read, DWORD *read, int *err) +{ + BOOL ret; + + *err = 0; + if (!(ret = ReadFile (get_handle (), buf, to_read, read, 0))) + *err = GetLastError (); + syscall_printf ("%d (err %d) = ReadFile (%p, %p, to_read %u, read %u, 0)", + ret, *err, get_handle (), buf, to_read, *read); + return ret; +} + +/* See comment in write_file below. */ +BOOL +fhandler_dev_floppy::lock_partition (DWORD to_write) +{ + DWORD bytes_read; + + /* The simple case. We have only a single partition open anyway. + Try to lock the partition so that a subsequent write succeeds. + If there's some file handle open on one of the affected partitions, + this fails, but that's how it works... + The high partition major numbers don't have a partition 0. */ + if (get_major () == DEV_FLOPPY_MAJOR + || get_major () >= DEV_SD_HIGHPART_START || get_minor () % 16 != 0) + { + if (!DeviceIoControl (get_handle (), FSCTL_LOCK_VOLUME, + NULL, 0, NULL, 0, &bytes_read, NULL)) + { + debug_printf ("DeviceIoControl (FSCTL_LOCK_VOLUME) failed, %E"); + return FALSE; + } + return TRUE; + } + + /* The tricky case. We're writing to the entire disk. What this code + basically does is to find out if the current write operation affects + one or more partitions on the disk. If so, it tries to lock all these + partitions and stores the handles for a subsequent close(). */ + NTSTATUS status; + IO_STATUS_BLOCK io; + FILE_POSITION_INFORMATION fpi; + /* Allocate space for 4 times the maximum partition count we can handle. + The reason is that for *every* single logical drive in an extended + partition on an MBR drive, 3 filler entries with partition number set + to 0 are added into the partition table returned by + IOCTL_DISK_GET_DRIVE_LAYOUT_EX. The first of them reproduces the data + of the next partition entry, if any, except for the partiton number. + Then two entries with everything set to 0 follow. Well, the + documentation states that for MBR drives the number of partition entries + in the PARTITION_INFORMATION_EX array is always a multiple of 4, but, + nevertheless, how crappy is that layout? */ + const DWORD size = sizeof (DRIVE_LAYOUT_INFORMATION_EX) + + 4 * MAX_PARTITIONS * sizeof (PARTITION_INFORMATION_EX); + PDRIVE_LAYOUT_INFORMATION_EX pdlix = (PDRIVE_LAYOUT_INFORMATION_EX) + alloca (size); + BOOL found = FALSE; + + /* Fetch current file pointer position on disk. */ + status = NtQueryInformationFile (get_handle (), &io, &fpi, sizeof fpi, + FilePositionInformation); + if (!NT_SUCCESS (status)) + { + debug_printf ("NtQueryInformationFile(FilePositionInformation): %y", + status); + return FALSE; + } + /* Fetch drive layout to get start and end positions of partitions on disk. */ + if (!DeviceIoControl (get_handle (), IOCTL_DISK_GET_DRIVE_LAYOUT_EX, NULL, 0, + pdlix, size, &bytes_read, NULL)) + { + debug_printf ("DeviceIoControl(IOCTL_DISK_GET_DRIVE_LAYOUT_EX): %E"); + return FALSE; + } + /* Scan through partition info to find the partition(s) into which we're + currently trying to write. */ + PARTITION_INFORMATION_EX *ppie = pdlix->PartitionEntry; + for (DWORD i = 0; i < pdlix->PartitionCount; ++i, ++ppie) + { + /* A partition number of 0 denotes an extended partition or one of the + aforementioned filler entries. Just skip. */ + if (ppie->PartitionNumber == 0) + continue; + /* Check if our writing range affects this partition. */ + if (fpi.CurrentByteOffset.QuadPart < ppie->StartingOffset.QuadPart + + ppie->PartitionLength.QuadPart + && ppie->StartingOffset.QuadPart < fpi.CurrentByteOffset.QuadPart + + to_write) + { + /* Yes. Now check if we can handle it. We can only handle + up to MAX_PARTITIONS partitions. The partition numbering is + one-based, so we decrement the partition number by 1 when using + as index into the partition array. */ + DWORD &part_no = ppie->PartitionNumber; + if (part_no >= MAX_PARTITIONS) + return FALSE; + found = TRUE; + debug_printf ("%u %D->%D : %D->%D", part_no, + ppie->StartingOffset.QuadPart, + ppie->StartingOffset.QuadPart + + ppie->PartitionLength.QuadPart, + fpi.CurrentByteOffset.QuadPart, + fpi.CurrentByteOffset.QuadPart + to_write); + /* Do we already have partitions? If not, create it. */ + if (!partitions) + { + partitions = (part_t *) ccalloc_abort (HEAP_FHANDLER, 1, + sizeof (part_t)); + partitions->refcnt = 1; + } + /* Next, check if the partition is already open. If so, skip it. */ + if (partitions->hdl[part_no - 1]) + continue; + /* Now open the partition and lock it. */ + WCHAR part[MAX_PATH], *p; + NTSTATUS status; + UNICODE_STRING upart; + OBJECT_ATTRIBUTES attr; + IO_STATUS_BLOCK io; + + sys_mbstowcs (part, MAX_PATH, get_win32_name ()); + p = wcschr (part, L'\0') - 1; + __small_swprintf (p, L"%d", part_no); + RtlInitUnicodeString (&upart, part); + InitializeObjectAttributes (&attr, &upart, + OBJ_CASE_INSENSITIVE + | ((get_flags () & O_CLOEXEC) + ? 0 : OBJ_INHERIT), + NULL, NULL); + status = NtOpenFile (&partitions->hdl[part_no - 1], + GENERIC_READ | GENERIC_WRITE, &attr, + &io, FILE_SHARE_READ | FILE_SHARE_WRITE, 0); + if (!NT_SUCCESS (status)) + { + debug_printf ("NtCreateFile(%W): %y", part, status); + return FALSE; + } + if (!DeviceIoControl (partitions->hdl[part_no - 1], FSCTL_LOCK_VOLUME, + NULL, 0, NULL, 0, &bytes_read, NULL)) + { + debug_printf ("DeviceIoControl (%W, FSCTL_LOCK_VOLUME) " + "failed, %E", part); + return FALSE; + } + } + } + /* If we didn't find a single matching partition, the "Access denied" + had another reason, so return FALSE in that case. */ + return found; +} + +BOOL +fhandler_dev_floppy::write_file (const void *buf, DWORD to_write, + DWORD *written, int *err) +{ + BOOL ret; + + *err = 0; + if (!(ret = WriteFile (get_handle (), buf, to_write, written, 0))) + *err = GetLastError (); + /* When writing to a disk or partition an "Access denied" error may + occur due to the raw disk write restriction. + See http://support.microsoft.com/kb/942448 for details. + What we do here is to lock the affected partition(s) and retry. */ + if (*err == ERROR_ACCESS_DENIED + && get_major () != DEV_CDROM_MAJOR + && (get_flags () & O_ACCMODE) != O_RDONLY + && lock_partition (to_write)) + { + *err = 0; + if (!(ret = WriteFile (get_handle (), buf, to_write, written, 0))) + *err = GetLastError (); + } + syscall_printf ("%d (err %d) = WriteFile (%p, %p, write %u, written %u, 0)", + ret, *err, get_handle (), buf, to_write, *written); + return ret; +} + +int +fhandler_dev_floppy::open (int flags, mode_t) +{ + int ret = fhandler_dev_raw::open (flags); + + if (ret) + { + DWORD bytes_read; + + if (get_drive_info (NULL)) + { + close (); + return 0; + } + if (!(flags & O_DIRECT)) + { + /* Create sector-aligned buffer. As default buffer size, we're using + some big, sector-aligned value. Since direct blockdev IO is + usually non-buffered and non-cached, the performance without + buffering is worse than access to a file system on same device. + Whoever uses O_DIRECT has my condolences. */ + devbufsiz = MAX (16 * bytes_per_sector, 65536); + devbufalloc = new char [devbufsiz + devbufalign]; + devbuf = (char *) roundup2 ((uintptr_t) devbufalloc, + (uintptr_t) devbufalign); + } + + /* If we're not trying to access a floppy disk, make sure we're actually + allowed to read *all* of the device or volume. This is actually + documented in the MSDN CreateFile man page. */ + if (get_major () != DEV_FLOPPY_MAJOR + && !DeviceIoControl (get_handle (), FSCTL_ALLOW_EXTENDED_DASD_IO, + NULL, 0, NULL, 0, &bytes_read, NULL)) + debug_printf ("DeviceIoControl (FSCTL_ALLOW_EXTENDED_DASD_IO) " + "failed, %E"); + } + + return ret; +} + +int +fhandler_dev_floppy::close () +{ + int ret = fhandler_dev_raw::close (); + + if (partitions && InterlockedDecrement (&partitions->refcnt) == 0) + { + for (int i = 0; i < MAX_PARTITIONS; ++i) + if (partitions->hdl[i]) + NtClose (partitions->hdl[i]); + cfree (partitions); + } + return ret; +} + +int +fhandler_dev_floppy::dup (fhandler_base *child, int flags) +{ + int ret = fhandler_dev_raw::dup (child, flags); + + if (!ret && partitions) + InterlockedIncrement (&partitions->refcnt); + return ret; +} + +inline off_t +fhandler_dev_floppy::get_current_position () +{ + IO_STATUS_BLOCK io; + FILE_POSITION_INFORMATION fpi = { { QuadPart : 0LL } }; + + NtQueryInformationFile (get_handle (), &io, &fpi, sizeof fpi, + FilePositionInformation); + return fpi.CurrentByteOffset.QuadPart; +} + +void +fhandler_dev_floppy::raw_read (void *ptr, size_t& ulen) +{ + DWORD bytes_read = 0; + DWORD read2; + DWORD bytes_to_read; + int ret; + size_t len = ulen; + char *tgt; + char *p = (char *) ptr; + + /* Checking a previous end of media */ + if (eom_detected () && !lastblk_to_read ()) + { + set_errno (ENOSPC); + goto err; + } + + if (devbuf) + { + while (len > 0) + { + if (devbufstart < devbufend) + { + bytes_to_read = MIN (len, devbufend - devbufstart); + debug_printf ("read %u bytes from buffer (rest %u)", + bytes_to_read, + devbufend - devbufstart - bytes_to_read); + memcpy (p, devbuf + devbufstart, bytes_to_read); + len -= bytes_to_read; + p += bytes_to_read; + bytes_read += bytes_to_read; + devbufstart += bytes_to_read; + + if (lastblk_to_read ()) + { + lastblk_to_read (false); + break; + } + } + if (len > 0) + { + if (len >= devbufsiz) + { + bytes_to_read = (len / bytes_per_sector) * bytes_per_sector; + tgt = p; + } + else + { + tgt = devbuf; + bytes_to_read = devbufsiz; + } + off_t current_position = get_current_position (); + if (current_position + bytes_to_read >= drive_size) + bytes_to_read = drive_size - current_position; + if (!bytes_to_read) + break; + + debug_printf ("read %u bytes from pos %U %s", bytes_to_read, + current_position, + len < devbufsiz ? "into buffer" : "directly"); + if (!read_file (tgt, bytes_to_read, &read2, &ret)) + { + if (!IS_EOM (ret)) + { + __seterrno (); + goto err; + } + + eom_detected (true); + + if (!read2) + { + if (!bytes_read) + { + debug_printf ("return -1, set errno to ENOSPC"); + set_errno (ENOSPC); + goto err; + } + break; + } + lastblk_to_read (true); + } + if (!read2) + break; + if (tgt == devbuf) + { + devbufstart = 0; + devbufend = read2; + } + else + { + len -= read2; + p += read2; + bytes_read += read2; + } + } + } + } + else + { + off_t current_position = get_current_position (); + bytes_to_read = len; + if (current_position + bytes_to_read >= drive_size) + bytes_to_read = drive_size - current_position; + debug_printf ("read %u bytes from pos %U directly", bytes_to_read, + current_position); + if (bytes_to_read && !read_file (p, bytes_to_read, &bytes_read, &ret)) + { + if (!IS_EOM (ret)) + { + __seterrno (); + goto err; + } + if (bytes_read) + eom_detected (true); + else + { + debug_printf ("return -1, set errno to ENOSPC"); + set_errno (ENOSPC); + goto err; + } + } + } + + ulen = (size_t) bytes_read; + return; + +err: + ulen = (size_t) -1; +} + +ssize_t +fhandler_dev_floppy::raw_write (const void *ptr, size_t len) +{ + DWORD bytes_written = 0; + char *p = (char *) ptr; + int ret; + + /* Checking a previous end of media */ + if (eom_detected ()) + { + set_errno (ENOSPC); + return -1; + } + + if (!len) + return 0; + + if (devbuf) + { + DWORD cplen, written; + + /* First check if we have an active read buffer. If so, try to fit in + the start of the input buffer and write out the entire result. + This also covers the situation after lseek since lseek fills the read + buffer in case we seek to an address which is not sector aligned. */ + if (devbufend && devbufstart < devbufend) + { + off_t current_pos = get_current_position (); + cplen = MIN (len, devbufend - devbufstart); + memcpy (devbuf + devbufstart, p, cplen); + LARGE_INTEGER off = { QuadPart:current_pos - devbufend }; + if (!SetFilePointerEx (get_handle (), off, NULL, FILE_BEGIN)) + { + devbufstart = devbufend = 0; + __seterrno (); + return -1; + } + if (!write_file (devbuf, devbufend, &written, &ret)) + { + devbufstart = devbufend = 0; + goto err; + } + /* Align pointers, lengths, etc. */ + cplen = MIN (cplen, written); + devbufstart += cplen; + if (devbufstart >= devbufend) + devbufstart = devbufend = 0; + p += cplen; + len -= cplen; + bytes_written += cplen; + } + /* As long as there's still something left in the input buffer ... */ + while (len) + { + /* Compute the length to write. The problem is that the underlying + driver may require sector aligned read/write. So we copy the data + over to devbuf, which is guaranteed to be sector aligned. */ + cplen = MIN (len, devbufsiz); + if (cplen >= bytes_per_sector) + /* If the remaining len is >= sector size, write out the maximum + possible multiple of the sector size which fits into devbuf. */ + cplen = rounddown (cplen, bytes_per_sector); + else + { + /* If len < sector size, read in the next sector, seek back, + and just copy the new data over the old one before writing. */ + LARGE_INTEGER off = { QuadPart:get_current_position () }; + if (!read_file (devbuf, bytes_per_sector, &written, &ret)) + goto err; + if (!SetFilePointerEx (get_handle (), off, NULL, FILE_BEGIN)) + { + __seterrno (); + return -1; + } + } + memcpy (devbuf, p, cplen); + if (!write_file (devbuf, MAX (cplen, bytes_per_sector), &written, + &ret)) + { + bytes_written += MIN (cplen, written); + goto err; + } + cplen = MIN (cplen, written); + p += cplen; + len -= cplen; + bytes_written += cplen; + /* If we overwrote, revalidate devbuf. It still contains the + content from the above read/modify/write. Revalidating makes + sure lseek reports the correct position. */ + if (cplen < bytes_per_sector) + { + devbufstart = cplen; + devbufend = bytes_per_sector; + } + } + return (ssize_t) bytes_written; + } + + /* In O_DIRECT case, just write. */ + if (write_file (p, len, &bytes_written, &ret)) + return (ssize_t) bytes_written; + +err: + if (IS_EOM (ret)) + { + eom_detected (true); + if (!bytes_written) + set_errno (ENOSPC); + } + else if (!bytes_written) + __seterrno (); + /* Cast is required, otherwise the error return value is (DWORD)-1. */ + return (ssize_t) bytes_written ?: -1; +} + +off_t +fhandler_dev_floppy::lseek (off_t offset, int whence) +{ + char buf[bytes_per_sector]; + off_t current_pos = (off_t) -1; + LARGE_INTEGER sector_aligned_offset; + size_t bytes_left; + + if (whence == SEEK_END) + { + offset += drive_size; + whence = SEEK_SET; + } + else if (whence == SEEK_CUR) + { + current_pos = get_current_position (); + off_t exact_pos = current_pos - (devbufend - devbufstart); + /* Shortcut when used to get current position. */ + if (offset == 0) + return exact_pos; + offset += exact_pos; + whence = SEEK_SET; + } + + if (whence != SEEK_SET || offset < 0 || offset > drive_size) + { + set_errno (EINVAL); + return -1; + } + + /* If new position is in buffered range, adjust buffer and return */ + if (devbufstart < devbufend) + { + if (current_pos == (off_t) -1) + current_pos = get_current_position (); + if (current_pos - devbufend <= offset && offset <= current_pos) + { + devbufstart = devbufend - (current_pos - offset); + return offset; + } + } + + sector_aligned_offset.QuadPart = rounddown (offset, bytes_per_sector); + bytes_left = offset - sector_aligned_offset.QuadPart; + + /* Invalidate buffer. */ + devbufstart = devbufend = 0; + + if (!SetFilePointerEx (get_handle (), sector_aligned_offset, NULL, + FILE_BEGIN)) + { + __seterrno (); + return -1; + } + + eom_detected (false); + + if (bytes_left) + { + raw_read (buf, bytes_left); + if (bytes_left == (size_t) -1) + return -1; + } + + return sector_aligned_offset.QuadPart + bytes_left; +} + +int +fhandler_dev_floppy::ioctl (unsigned int cmd, void *buf) +{ + int ret = 0; + DWORD bytes_read; + + switch (cmd) + { + case HDIO_GETGEO: + debug_printf ("HDIO_GETGEO"); + ret = get_drive_info ((struct hd_geometry *) buf); + break; + case BLKGETSIZE: + case BLKGETSIZE64: + debug_printf ("BLKGETSIZE"); + if (cmd == BLKGETSIZE) + *(long *)buf = drive_size >> 9UL; + else + *(off_t *)buf = drive_size; + break; + case BLKRRPART: + debug_printf ("BLKRRPART"); + if (!DeviceIoControl (get_handle (), IOCTL_DISK_UPDATE_PROPERTIES, + NULL, 0, NULL, 0, &bytes_read, NULL)) + { + __seterrno (); + ret = -1; + } + else + get_drive_info (NULL); + break; + case BLKSSZGET: + debug_printf ("BLKSSZGET"); + *(int *)buf = (int) bytes_per_sector; + break; + case BLKIOMIN: + debug_printf ("BLKIOMIN"); + *(int *)buf = (int) bytes_per_sector; + break; + case BLKIOOPT: + debug_printf ("BLKIOOPT"); + *(int *)buf = (int) bytes_per_sector; + break; + case BLKPBSZGET: + debug_printf ("BLKPBSZGET"); + *(int *)buf = (int) bytes_per_sector; + break; + case BLKALIGNOFF: + debug_printf ("BLKALIGNOFF"); + *(int *)buf = 0; + break; + default: + ret = fhandler_dev_raw::ioctl (cmd, buf); + break; + } + return ret; +} + |