訪問流
PHP用戶空間中所有的檔案I/O處理都是透過php 4.3引入的php流包裝層處理的. 在內部擴充程式碼可以選擇使用檔處理和本地文件系統或伯克利域套接字進行通信, 或者也可以調用和用戶空間流I/O相同的API.
流的概覽
, 直接的文件描述通常相描述比調用流包裝層消耗更少的CPU和內存; 然而, 這樣會將實現某個特定協議的所有工作都堆積到作為擴展開發者的你身上. 通過掛鉤到流包裝層, 你的擴展代碼可以透明的使用各種內建的流包裝, 比如HTTP, FTP, 以及它們對應的SSL版本, 另外還有gzip和bzip2壓縮包裝. 通過include特定的PEAR或PECL模組, 你的代碼還可以訪問其他協議, 比如SSH2, WebDav, 甚至是Gopher!
本章將介紹內部基於流工作的基礎API. 後面到第16章"有趣的流"中, 我們將看到諸如應用過濾器, 使用上下文選項和參數等高級概念.
打開流
儘管是一個統一的API, 但實際上依賴於所需的流的類型, 有四種不同的路徑去打開一個流. 從用戶空間角度來看, 這四種不同的類別如下(函數列表只代表示例, 不是完整列表):
<?php /* fopen包装 * 操作文件/URI方式指定远程文件类资源 */ $fp = fopen($url, $mode); $data = file_get_contents($url); file_put_contents($url, $data); $lines = file($url); /* 传输 * 基于套接字的顺序I/O */ $fp = fsockopen($host, $port); $fp = stream_socket_client($uri); $fp = stream_socket_server($uri, $options); /* 目录流 */ $dir = opendir($url); $files = scandir($url); $obj = dir($url); /* "特殊"的流 */ $fp = tmpfile(); $fp = popen($cmd); proc_open($cmd, $pipes); ?>
無論你打開的是什麼類型的流, 它們都存儲在一個公共的結構體php_stream中.
fopen包裝
我們首先從實現fopen()函數開始. 現在你應該已經對創建擴展骨架很熟悉了, 如果還不熟悉, 請回到第5章"你的第一個擴展"複習一下, 以下是我們實作的fopen()函數:
PHP_FUNCTION(sample5_fopen) { php_stream *stream; char *path, *mode; int path_len, mode_len; int options = ENFORCE_SAFE_MODE | REPORT_ERRORS; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &path, &path_len, &mode, &mode_len) == FAILURE) { return; } stream = php_stream_open_wrapper(path, mode, options, NULL); if (!stream) { RETURN_FALSE; } php_stream_to_zval(stream, return_value); }
php_stream_open_wrapper()的目的應該是完全繞過底層. path指定要讀寫檔名或URL, 讀寫行為依賴於mode的值.
options是位域的標記值集合, 這裡是設定為下面介紹的一組固定值:
USE_PATH將php.ini檔案中的include_path上. 內建函數fopen()在指定第三個參數為TRUE
時將會設定這個選項.
STREAM_USE_URL
STREAM_USE_URLSTREAM_USE_URL
php://, file://, zlib://, bzip2://這些URL包裝器並不會
認為它們是遠端URL.P.CE
儘管這個常數這樣命名, 但實際上設定這個選項 後僅僅是啟用了安全模式(php.ini檔案中的 safe_mode指令)的強制檢查. 如果沒有設定這個 中safe_mode如何設定) REPORT_ERRORS . STREAM_MUST_SEEK對於某些流, 例如套接字, 是不可以seek的(隨機 ); seek. 如果調用作用域指定這個選項, 並且包裝 器檢測到它不能保證可以seek, 將會拒絕打開這 _Wk呼叫作用域要求流可以被轉換到stdio或 posix檔案描述子, 則應該給open_wrapper函數 傳遞這個選項, 以保證在失敗/O完成操作之前就保證在失敗/O 標識只需要從流中請求元資料. 實際上這是用於 http包裝器, 獲取http_response_http包裝器, 獲取http_response_ http包裝器, 獲取http_response_量的全域變體內容. STREAM_DISABLE_OPEN_BASEDIR類似safe_mode檢查, 不設定這個選項會檢查 的檢查 告知流包裝層, 所有內部分配的空間都採用持久 化分配, 並將關聯的資源註冊到持久化列表中。則搜尋預設的包含路徑.多數URL 包裝器都忽略這個選項. IGNORE_URL提供这个选项时, 流包装层只打开本地文件. 所 有的is_url包装器都将被忽略. 最后的NULL参数是char **类型, 它最初是用来设置匹配路径, 如果path指向普通文件URL, 则去掉file://部分, 保留直接的文件路径用于传统的文件名操作. 这个参数仅仅是以前引擎内部处理使用的. 此外, 还有php_stream_open_wrapper()的一个扩展版本: 最后一个参数context允许附加的控制, 并可以得到包装器内的通知. 你将在第16章看到这个参数的细节. 传输层包装 尽管传输流和fopen包装流是相同的组件组成的, 但它的注册策略和其他的流不同. 从某种程度上来说, 这是因为用户空间对它们的访问方式的不同造成的, 它们需要实现基于套接字的其他因子. 从扩展开发者角度来看, 打开传输流的过程是相同的. 下面是对fsockopen()的实现: 这个函数的基础构造和前面的fopen示例是一样的. 不同在于host和端口号使用不同的参数指定, 接着为了给出一个传输流URL就必须将它们合并到一起. 在产生了一个有意义的"路径:后, 将它传递给php_stream_xport_create()函数, 方式和fopen()使用的php_stream_open_wrapper()API一样. php_stream_xport_create()的原型如下: 每个参数的含义如下: xport基于URI的传输描述符. 对于基于inet的套接字流, 它可以是 tcp://127.0.0.1:80, udp://10.0.0.1:53, ssl://169.254.13.24:445等. 此 外, UNIX域传输协议unix:///path/to/socket, udg:///path/to/dgramsocket等都是合法的. xport_len指定了xport的长 度, 因此xport是二进制安全的. options这个值是由前面php_stream_open_wrapper()中介绍的选项通过按位 或组成的值. flags由STREAM_XPORT_CLIENT或STREAM_XPORT_SERVER之一 与下面另外一张表中将列出的STREAM_XPORT_*常量通过按位或 组合得到的值. persistent_id如果請求的傳輸流需要在請求間持久化, 呼叫作用域可以提供一個 key名字描述連接. 指定這個值為NULL創建非持久化連接;key名字描述連接.指定這個值為NULL創建非持久化連接; 一的字串值將嘗試先從持久化池中尋找已有的傳輸流, 或沒有 找到時就創建一個新的持久化流 找到時就創建一個新的持久化流.在逾時回傳失敗之前連線的嘗試時間. 如果這個值傳遞為NULL則使 用php.ini中指定的預設逾時值. 這個參數對服務端傳輸流沒有意義.errstr 如果在選定的套接字上建立, 連接, 綁定或監聽時發生錯誤, 這裡傳遞 的char *引用值將被設定為一個描述發生錯誤原因的字串. errstr初 始應該指向的是NULL; 如果在返回時它被設定了值, 則調用作用域有 透過errstr傳回的錯誤訊息對應的數值錯誤碼. php_stream_xport_create()的flags參數中使用了STREAM_XPORT_*一族常數定義如下:建立連接. 這個 標記通常和STREAM_XPORT_CONNECT或 STREAM_XPORT_CON STREAM_XPORT_SERVER 本地端將透過傳輸層accept連接. 這個標記通常 和STREAM_XPORT_BIND以及 STREAM_XPORT_LISTEN一起使用.。 分. 在建立客戶端傳輸流時省略這個標記是合法 的, 但是這樣做就要求手動的調用 php_stream_xport_connect(). STREAM_XPORT_CONNECT_ASYNC尝试连接到远程资源, 但不阻塞. STREAM_XPORT_BIND将传输流绑定到本地资源. 用在服务端传输流时, 这将使得accept连接的传输流准备端口, 路径或 特定的端点标识符等信息. STREAM_XPORT_LISTEN在已绑定的传输流端点上监听到来的连接. 这通 常用于基于流的传输协议, 比如: tcp://, ssl://, unix://. 目录访问 fopen包装器支持目录访问, 比如file://和ftp://, 还有第三种流打开函数也可以用于目录访问, 下面是对opendir()的实现: 同样的, 也可以为某个特定目录打开一个流, 比如本地文件系统的目录名或支持目录访问的URL格式资源. 这里我们又看到了options参数, 它和原来的含义一样, 第三个参数NULL原型是php_stream_context类型. 在目录流打开后, 和文件以及传输流一样, 返回给用户空间. 特殊流 还有一些特殊类型的流不能归类到fopen/transport/directory中. 它们中每一个都有自己独有的API: 创建一个可seek的缓冲区流用于读写. 在关闭时, 这个流使用的所有临时资源, 包括所有的缓冲区(无论是在内存还是磁盘), 都将被释放. 使用这一组API中的后一个函数, 允许临时文件被以特定的格式命名放到指定路径. 这些内部API调用被用户空间的tmpfile()函数隐藏. 这3个API方法接受已经打开的FILE *资源或文件描述符ID, 使用流API的某种操作包装. fd格式的接口不会搜索匹配你前面看到过的fopen函数打开的资源, 但是它会注册持久化的资源, 后续的fopen可以使用到这个持久化资源. 访问流 在你打开一个流之后, 就可以在它上面执行I/O操作了. 使用那种协议包装API创建了流并不重要, 它们都使用相同的访问API. 读 流的读写可以使用下面的API函数组合完成, 它们多数都是遵循POSIX I/O中对应的API规范的: 从数据流中接收一个字符. 如果流上再没有数据, 则返回EOF. 从指定流中读取指定字节的数据. buf必须预分配至少count字节的内存空间. 这个函数将返回从数据流实际读到缓冲区中的数据字节数. php_stream_read()不同于其他的流读取函数. 如果使用的流不是普通文件流, 哪怕数据流中有超过请求字节数的数据, 并且当前也可以返回, 它也只会调用过一次底层流实现的read函数. 这是为了兼容基于包(比如UDP)的协议的这种做法. 这两个函数从stream中读取最多maxlen个字符, 直到碰到换行符或流结束. buf可以是一个指向预分配的至少maxlen字节的内存空间的指针, 也可以是NULL, 当它是NULL时, 则会自动的创建一个动态大小的缓冲区, 用从流中实际读出的数据填充, 成功后函数返回指向缓冲区的指针, 失败则返回NULL. 如果returned_len传递了非NULL值, 则在返回时它将被设置为实际从流中读取的字节数. 和php_stream_get_line()类似, 这个函数将读取最多maxlen, 或到达EOF/行结束第一次出现的位置. 但是它也有和php_stream_get_line()的不同指出, 这个函数允许指定任意的停止读取标记. 读取目录项 从php流中读取目录项和上面从普通文件中读取普通数据相同. 这些数据放到了固定大小的dirents块中. 内部的php_stream_dirent结构体如下, 它与POSIX定义的dirent结构体一致: 实际上你可以直接使用php_stream_read()函数读取数据到这个结构体中: 由于从目录流中读取是很常见的操作, php流包装层暴露了一个API, 它将记录大小的检查和类型转换处理封装到了一次调用中: 如果成功读取到目录项, 则传入的entry指针将被返回, 否则返回NULL标识错误. 使用这个为目录流特殊构建的函数而不是直接从目录流读取非常重要, 这样做未来流API改变时就不至于和你的代码冲突. 写 和读类似, 向流中写数据只需要传递一个缓冲区和缓冲区长度给流. write_string的版本实际上是一个提供便利的宏, 它允许写一个NULL终止的字符串,而不用显式的提供长度. 返回的是实际写到流中的字节数. 要特别小心的是尝试写大数据的时候可能导致流阻塞, 比如套接字流, 而如果流被标记为非阻塞, 则实际写入的数据量可能会小于传递给函数的期望大小. 还有一种选择是, 使用php_stream_putc()和php_stream_puts()写入一个字符或一个字符串到流中. 要注意, php_stream_puts()不同于php_stream_write_string(), 虽然它们的原型看起来是一样的, 但是php_stream_puts()会在写出buf中的数据后自动的追加一个换行符. 功能和格式上都类似于fprintf(), 这个API调用允许在写的同时构造字符串而不用去创建临时缓冲区构造数据. 这里我们能够看到的一个明显的不同是它需要TSRMLS_CC宏来保证线程安全. 随机访问, 查看文件偏移量以及缓存的flush 基于文件的流, 以及另外几种流是可以随机访问的. 也就是说, 在流的一个位置读取了一些数据之后, 文件指针可以向前或向后移动, 以非线性顺序读取其他部分. 如果你的流应用代码预测到底层的流支持随机访问, 在打开的时候就应该传递STREAM_MUST_SEEK选项. 对于那些原本就可随机访问的流来说, 这通常不会有什么影响, 因为流本身就是可随机访问的. 而对于那些原本不可随机访问的流, 比如网络I/O或线性访问文件比如FIFO管道, 这个暗示可以让调用程序有机会在流的数据被消耗掉之前, 优雅的失败. 在可随机访问的流资源上工作时, 下面的函数可用来将文件指针移动到任意位置: offset是相对于whence表示的流位置的偏移字节数, whence的可选值及含义如下: SEEK_SEToffset相对于文件开始位置. php_stream_rewind()API调用实际上是一个宏, 展开后是php_stream_seek(stream, 0, SEEK_SET), 表示移动到文件开始 位置偏移0字节处. 当使用SEEK_SET时, 如果offset传递负值被认为是错误 的, 将会导致未定义行为. 指定的位置超过流的末尾也是未定义的, 不过结果 通常是一个错误或文件被扩大以满足指定的偏移量. SEEK_CURoffset相对于文件的当前偏移量. 调用php_stream_seek(steram, offset, SEEK_CUR)一般来说等价于php_stream_seek(stream, php_stream_tell() + offset, SEEK_SET); SEEK_ENDoffset是相对于当前的EOF位置的. 负值的offset表示在EOF之前的位置, 正 值和SEEK_SET中描述的是相同的语义, 可能在某些流实现上可以工作. 在目录流上随机访问时, 只有php_stream_rewinddir()函数可用. 使用php_stream_seek()函数将导致未定义行为. 所有的随机访问一族函数返回0标识成功或者-1标识失败. 如你之前所见, php_stream_tell()将返回当前的文件偏移量. 调用flush()函数将强制将流过滤器此类内部缓冲区中的数据输出到最终的资源中. 在流被关闭时, flush()函数将自动调用, 并且大多数无过滤流资源虽然不进行任何内部缓冲, 但也需要flush. 显式的调用这个函数很少见, 并且通常也是不需要的. 调用php_stream_stat()可以获取到流实例的其他信息, 它的行为类似于fstat()函数. 实际上, php_stream_statbuf结构体现在仅包含一个元素: struct statbuf sb; 因此, php_stream_stat()调用可以如下面例子一样, 直接用传统的fstat()操作替代, 它只是将posix的stat操作翻译成流兼容的: 关闭 所有流的关闭都是通过php_stream_free()函数处理的, 它的原型如下: 这个函数中的options参数允许的值是PHP_STREAM_FREE_xxx一族常量的按位或的结果, 这一族常量定义如下(下面省略PHP_STREAM_FREE_前缀): CALL_DTOR流实现的析构器应该被调用. 这里提供了一个时机对特定的流 进行显式释放. RELEASE_STREAM释放为php_stream结构体分配的内存 PRESERVE_HANDLE指示流的析构器不要关闭它的底层描述符句柄 RSRC_DTOR流包装层内部管理资源列表的垃圾回收 PERSISTENT作用在持久化流上时, 它的行为将是永久的而不局限于当前请 求. CLOSECALL_DTOR和RELEASE_STREAM的联合. 这是关闭 非持久化流的一般选项. CLOSE_CASTEDCLOSE和PRESERVE_HANDLE的联合. CLOSE_PERSISTENTCLOSE和PERSISTENT的联合. 这是永久关闭持久化流的一 般选项. 实际上, 你并不需要直接调用php_stream_free()函数. 而是在关闭流时使用下面两个宏的某个替代: 通过zval交换流 因为流通常映射到zval上, 反之亦然, 因此提供了一组宏用来简化操作, 并统一编码(格式): 要注意, 这里并没有调用ZEND_REGISTER_RESOURCE(). 这是因为当流打开的时候, 已经自动的注册为资源了, 这样就可以利用到引擎内建的垃圾回收和shutdown系统的优点. 使用这个宏而不是尝试手动的将流注册为新的资源ID是非常重要的; 这样做的最终结果是导致流被关闭两次以及引擎崩溃. 从传入的zval *中取回php_stream *有一个类似的宏. 可以看出, 这个宏只是对资源获取函数(第9章"资源数据类型")的一个简单封装. 请回顾ZEND_FETCH_RESOURCE2()宏, 第一个宏php_stream_from_zval()就是对它的包装, 如果资源类型不匹配, 它将抛出一个警告并尝试从函数实现中返回. 如果你只是想从传入的zval *中获取一个php_stream *, 而不希望有自动的错误处理, 就需要使用php_stream_from_zval_no_verify()并且需要手动的检查结果值.
静态资源操作 一个基于流的原子操作并不需要实际的实例. 下面这些API仅仅使用URL执行这样的操作: 和前面的php_stream_stat()类似, 这个函数提供了一个对POSIX的stat()函数协议依赖的包装. 要注意, 并不是所有的协议都支持URL记法, 并且即便支持也可能不能报告出statbuf结构体中的所有成员值. 一定要检查php_stream_stat_path()失败时的返回值, 0标识成功, 要知道, 不支持的元素返回时其值将是默认的0. 这个php_stream_url_stat()的扩展版本允许传递另外两个参数. 第一个是flags, 它的值可以是下面的PHP_STERAM_URL_STAT_*(下面省略PHP_STREAM_URL_STAT_前缀)一族常量的按位或的结果. 还有一个是context参数, 它在其他的一些流函数中也有出现, 我们将在第16章去详细学习. LINK原始的php_stream_stat_path()对于符号链接或目录将会进行解析直到碰到 协议定义的结束资源. 传递PHP_STREAM_URL_STAT_LINK标记将导致 php_stream_stat_path()返回请求资源的信息而不会进行符号链接的解析. (译注: 我们可以这样理解, 没有这个标记, 底层使用stat(), 如果有这个标记, 底层使用lstat(), 关于stat()和lstat()的区别, 请查看*nix手册) QUIET默认情况下, 如果在执行URL的stat操作过程中碰到错误, 包括文件未找到错 误, 都将通过php的错误处理机制触发. 传递QUIET标记可以使得 php_stream_stat_path()返回而不报告错误. 创建和删除目录也会如你期望的工作. 这里的options参数和前面的php_stream_open_wrapper()函数的同名参数含义一致. 对于php_stream_mkdir(), 还有一个参数mode用于指定一个八进制的值表明读写执行权限. 小结 本章中你接触了一些基于流的I/O的内部表象. 下一章将演示做呢样实现自己的协议包装, 甚至是定义自己的流类型. 以上就是[翻译][php扩展开发和嵌入式]第14章-php中流的访问的内容,更多相关内容请关注PHP中文网(www.php.cn)!php_stream *php_stream_open_wrapper_ex(char *path,
char *mode, int options, char **opened_path,
php_stream_context *context);
PHP_FUNCTION(sample5_fsockopen)
{
php_stream *stream;
char *host, *transport, *errstr = NULL;
int host_len, transport_len, implicit_tcp = 1, errcode = 0;
long port = 0;
int options = ENFORCE_SAFE_MODE;
int flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|l",
&host, &host_len, &port) == FAILURE) {
return;
}
if (port) {
int implicit_tcp = 1;
if (strstr(host, "://")) {
/* A protocol was specified,
* no need to fall back on tcp:// */
implicit_tcp = 0;
}
transport_len = spprintf(&transport, 0, "%s%s:%d",
implicit_tcp ? "tcp://" : "", host, port);
} else {
/* When port isn't specified
* we can safely assume that a protocol was
* (e.g. unix:// or udg://) */
transport = host;
transport_len = host_len;
}
stream = php_stream_xport_create(transport, transport_len,
options, flags,
NULL, NULL, NULL, &errstr, &errcode);
if (transport != host) {
efree(transport);
}
if (errstr) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "[%d] %s",
errcode, errstr);
efree(errstr);
}
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
php_stream *php_stream_xport_create(char *xport, int xport_len,
int options, int flags,
const char *persistent_id,
struct timeval *timeout,
php_stream_context *context,
char **errstr, int *errcode);
PHP_FUNCTION(sample5_opendir)
{
php_stream *stream;
char *path;
int path_len, options = ENFORCE_SAFE_MODE | REPORT_ERRORS;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s",
&path, &path_len) == FAILURE) {
return;
}
stream = php_stream_opendir(path, options, NULL);
if (!stream) {
RETURN_FALSE;
}
php_stream_to_zval(stream, return_value);
}
php_stream *php_stream_fopen_tmpfile(void);
php_stream *php_stream_fopen_temporary_file(const char *dir,
const char *pfx, char **opened_path);
php_stream *php_stream_fopen_from_fd(int fd,
const char *mode, const char *persistent_id);
php_stream *php_stream_fopen_from_file(FILE *file,
const char *mode);
php_stream *php_stream_fopen_from_pipe(FILE *file,
const char *mode);
int php_stream_getc(php_stream *stream);
size_t php_stream_read(php_stream *stream, char *buf, size_t count);
char *php_stream_get_line(php_stream *stream, char *buf,
size_t maxlen, size_t *returned_len);
char *php_stream_gets(php_stream *stream, char *buf,
size_t maxlen);
char *php_stream_get_record(php_stream *stream,
size_t maxlen, size_t *returned_len,
char *delim, size_t delim_len
TSRMLS_DC);
typedef struct _php_stream_dirent {
char d_name[MAXPATHLEN];
} php_stream_dirent;
{
struct dirent entry;
if (php_stream_read(stream, (char*)&entry, sizeof(entry))
== sizeof(entry)) {
/* 成功从目录流中读取到一项 */
php_printf("File: %s\n", entry.d_name);
}
}
php_stream_dirent *php_stream_readdir(php_stream *dirstream,
php_stream_dirent *entry);
size_t php_stream_write(php_stream *stream, char *buf,
size_t count);
size_t php_stream_write_string(php_stream *stream, char *stf);
int php_stream_putc(php_stream *stream, int c);
int php_stream_puts(php_string *stream, char *buf);
size_t php_stream_printf(php_stream *stream TSRMLS_DC,
const char *format, ...);
int php_stream_seek(php_stream *stream, off_t offset, int whence);
int php_stream_rewind(php_stream *stream);
int php_stream_rewinddir(php_stream *dirstream);
off_t php_stream_tell(php_stream *stream);
int php_stream_flush(php_stream *stream);
int php_stream_stat(php_stream *stream, php_stream_statbuf *ssb);
int php_sample4_fd_is_fifo(int fd)
{
struct statbuf sb;
fstat(fd, &sb);
return S_ISFIFO(sb.st_mode);
}
int php_sample4_stream_is_fifo(php_stream *stream)
{
php_stream_statbuf ssb;
php_stream_stat(stream, &ssb);
return S_ISFIFO(ssb.sb.st_mode);
}
int php_stream_free(php_stream *stream, int options);
#define php_stream_close(stream) \
php_stream_free((stream), PHP_STREAM_FREE_CLOSE)
#define php_stream_pclose(stream) \
php_stream_free((stream), PHP_STREAM_FREE_CLOSE_PERSISTENT)
#define php_stream_to_zval(stream, pzval) \
ZVAL_RESOURCE((pzval), (stream)->rsrc_id);
#define php_stream_from_zval(stream, ppzval) \
ZEND_FETCH_RESOURCE2((stream), php_stream*, (ppzval), \
-1, "stream", php_file_le_stream(), php_file_le_pstream())
#define php_stream_from_zval_no_verify(stream, ppzval) \
(stream) = (php_stream*)zend_fetch_resource((ppzval) \
TSRMLS_CC, -1, "stream", NULL, 2, \
php_file_le_stream(), php_file_le_pstream())
int php_stream_stat_path(char *path, php_stream_statbuf *ssb);
int php_stream_stat_path_ex(char *path, int flags,
php_stream_statbuf *ssb, php_stream_context *context);
int php_stream_mkdir(char *path, int mode, int options,
php_stream_context *context);
int php_stream_rmdir(char *path, int options,
php_stream_context *context);