/src/libreoffice/sal/osl/unx/file_stat.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <osl/file.h> |
21 | | |
22 | | #include <sys/stat.h> |
23 | | #include <dirent.h> |
24 | | #include <errno.h> |
25 | | #include <limits.h> |
26 | | #include <unistd.h> |
27 | | #include <utime.h> |
28 | | |
29 | | #include <osl/diagnose.h> |
30 | | #include <osl/thread.h> |
31 | | |
32 | | #include "system.hxx" |
33 | | #include "file_impl.hxx" |
34 | | #include "file_error_transl.hxx" |
35 | | #include "file_path_helper.hxx" |
36 | | #include "file_url.hxx" |
37 | | #include "uunxapi.hxx" |
38 | | |
39 | | namespace |
40 | | { |
41 | | void set_file_type(const struct stat& file_stat, oslFileStatus* pStat) |
42 | 133k | { |
43 | | /* links to directories state also to be a directory */ |
44 | 133k | if (S_ISLNK(file_stat.st_mode)) |
45 | 0 | pStat->eType = osl_File_Type_Link; |
46 | 133k | else if (S_ISDIR(file_stat.st_mode)) |
47 | 9.82k | pStat->eType = osl_File_Type_Directory; |
48 | 123k | else if (S_ISREG(file_stat.st_mode)) |
49 | 123k | pStat->eType = osl_File_Type_Regular; |
50 | 0 | else if (S_ISFIFO(file_stat.st_mode)) |
51 | 0 | pStat->eType = osl_File_Type_Fifo; |
52 | 0 | else if (S_ISSOCK(file_stat.st_mode)) |
53 | 0 | pStat->eType = osl_File_Type_Socket; |
54 | 0 | else if (S_ISCHR(file_stat.st_mode) || S_ISBLK(file_stat.st_mode)) |
55 | 0 | pStat->eType = osl_File_Type_Special; |
56 | 0 | else |
57 | 0 | pStat->eType = osl_File_Type_Unknown; |
58 | | |
59 | 133k | pStat->uValidFields |= osl_FileStatus_Mask_Type; |
60 | 133k | } |
61 | | |
62 | | void set_file_access_mask(const struct stat& file_stat, oslFileStatus* pStat) |
63 | 133k | { |
64 | | // user permissions |
65 | 133k | if (S_IRUSR & file_stat.st_mode) |
66 | 133k | pStat->uAttributes |= osl_File_Attribute_OwnRead; |
67 | | |
68 | 133k | if (S_IWUSR & file_stat.st_mode) |
69 | 133k | pStat->uAttributes |= osl_File_Attribute_OwnWrite; |
70 | | |
71 | 133k | if (S_IXUSR & file_stat.st_mode) |
72 | 9.82k | pStat->uAttributes |= osl_File_Attribute_OwnExe; |
73 | | |
74 | | // group permissions |
75 | 133k | if (S_IRGRP & file_stat.st_mode) |
76 | 258 | pStat->uAttributes |= osl_File_Attribute_GrpRead; |
77 | | |
78 | 133k | if (S_IWGRP & file_stat.st_mode) |
79 | 9 | pStat->uAttributes |= osl_File_Attribute_GrpWrite; |
80 | | |
81 | 133k | if (S_IXGRP & file_stat.st_mode) |
82 | 42 | pStat->uAttributes |= osl_File_Attribute_GrpExe; |
83 | | |
84 | | // others permissions |
85 | 133k | if (S_IROTH & file_stat.st_mode) |
86 | 258 | pStat->uAttributes |= osl_File_Attribute_OthRead; |
87 | | |
88 | 133k | if (S_IWOTH & file_stat.st_mode) |
89 | 9 | pStat->uAttributes |= osl_File_Attribute_OthWrite; |
90 | | |
91 | 133k | if (S_IXOTH & file_stat.st_mode) |
92 | 42 | pStat->uAttributes |= osl_File_Attribute_OthExe; |
93 | | |
94 | 133k | pStat->uValidFields |= osl_FileStatus_Mask_Attributes; |
95 | 133k | } |
96 | | |
97 | | /* This code used not to use access(...) because access follows links which |
98 | | may cause performance problems see #97133. (That apparently references a |
99 | | no-longer accessible Hamburg-internal bug-tracking system.) |
100 | | However, contrary to what is stated above the use of access calls is |
101 | | required on network file systems not using unix semantics (AFS, see |
102 | | fdo#43095). |
103 | | */ |
104 | | void set_file_access_rights(const OString& file_path, oslFileStatus* pStat) |
105 | 9.82k | { |
106 | 9.82k | pStat->uValidFields |= osl_FileStatus_Mask_Attributes; |
107 | | |
108 | 9.82k | if (osl::access(file_path, W_OK) < 0) |
109 | 0 | pStat->uAttributes |= osl_File_Attribute_ReadOnly; |
110 | | |
111 | 9.82k | if (osl::access(file_path, X_OK) == 0) |
112 | 42 | pStat->uAttributes |= osl_File_Attribute_Executable; |
113 | 9.82k | } |
114 | | |
115 | | void set_file_hidden_status(const OString& file_path, oslFileStatus* pStat) |
116 | 133k | { |
117 | 133k | pStat->uAttributes = osl::systemPathIsHiddenFileOrDirectoryEntry(file_path) ? osl_File_Attribute_Hidden : 0; |
118 | 133k | pStat->uValidFields |= osl_FileStatus_Mask_Attributes; |
119 | 133k | } |
120 | | |
121 | | /* the set_file_access_rights must be called after set_file_hidden_status(...) and |
122 | | set_file_access_mask(...) because of the hack in set_file_access_rights(...) */ |
123 | | void set_file_attributes( |
124 | | const OString& file_path, const struct stat& file_stat, const sal_uInt32 uFieldMask, oslFileStatus* pStat) |
125 | 133k | { |
126 | 133k | set_file_hidden_status(file_path, pStat); |
127 | 133k | set_file_access_mask(file_stat, pStat); |
128 | | |
129 | | // we set the file access rights only on demand |
130 | | // because it's potentially expensive |
131 | 133k | if (uFieldMask & osl_FileStatus_Mask_Attributes) |
132 | 9.82k | set_file_access_rights(file_path, pStat); |
133 | 133k | } |
134 | | |
135 | | void set_file_access_time(const struct stat& file_stat, oslFileStatus* pStat) |
136 | 133k | { |
137 | 133k | pStat->aAccessTime.Seconds = file_stat.st_atime; |
138 | 133k | pStat->aAccessTime.Nanosec = 0; |
139 | 133k | pStat->uValidFields |= osl_FileStatus_Mask_AccessTime; |
140 | 133k | } |
141 | | |
142 | | void set_file_modify_time(const struct stat& file_stat, oslFileStatus* pStat) |
143 | 133k | { |
144 | 133k | pStat->aModifyTime.Seconds = file_stat.st_mtime; |
145 | 133k | pStat->aModifyTime.Nanosec = 0; |
146 | 133k | pStat->uValidFields |= osl_FileStatus_Mask_ModifyTime; |
147 | 133k | } |
148 | | |
149 | | void set_file_size(const struct stat& file_stat, oslFileStatus* pStat) |
150 | 133k | { |
151 | 133k | if (S_ISREG(file_stat.st_mode)) |
152 | 123k | { |
153 | 123k | pStat->uFileSize = file_stat.st_size; |
154 | 123k | pStat->uValidFields |= osl_FileStatus_Mask_FileSize; |
155 | 123k | } |
156 | 133k | } |
157 | | |
158 | | /* we only need to call stat or lstat if one of the |
159 | | following flags is set */ |
160 | | bool is_stat_call_necessary(sal_uInt32 field_mask, oslFileType file_type) |
161 | 133k | { |
162 | 133k | return ( |
163 | 133k | ((field_mask & osl_FileStatus_Mask_Type) && (file_type == osl_File_Type_Unknown)) || |
164 | 106k | (field_mask & osl_FileStatus_Mask_Attributes) || |
165 | 96.7k | (field_mask & osl_FileStatus_Mask_CreationTime) || |
166 | 96.7k | (field_mask & osl_FileStatus_Mask_AccessTime) || |
167 | 96.7k | (field_mask & osl_FileStatus_Mask_ModifyTime) || |
168 | 96.7k | (field_mask & osl_FileStatus_Mask_FileSize) || |
169 | 96.7k | (field_mask & osl_FileStatus_Mask_LinkTargetURL) || |
170 | 205 | (field_mask & osl_FileStatus_Mask_Validate)); |
171 | 133k | } |
172 | | |
173 | | oslFileError set_link_target_url(const OString& file_path, oslFileStatus* pStat) |
174 | 0 | { |
175 | 0 | OString link_target; |
176 | 0 | if (!osl::realpath(file_path, link_target)) |
177 | 0 | return oslTranslateFileError(errno); |
178 | | |
179 | 0 | OUString url; |
180 | 0 | oslFileError osl_error = osl::detail::convertPathnameToUrl(link_target, &url); |
181 | 0 | if (osl_error != osl_File_E_None) |
182 | 0 | return osl_error; |
183 | 0 | rtl_uString_assign(&pStat->ustrLinkTargetURL, url.pData); |
184 | |
|
185 | 0 | pStat->uValidFields |= osl_FileStatus_Mask_LinkTargetURL; |
186 | 0 | return osl_File_E_None; |
187 | 0 | } |
188 | | |
189 | | oslFileError setup_osl_getFileStatus( |
190 | | DirectoryItem_Impl * pImpl, oslFileStatus* pStat, OString& file_path) |
191 | 133k | { |
192 | 133k | if ((pImpl == nullptr) || (pStat == nullptr)) |
193 | 0 | return osl_File_E_INVAL; |
194 | | |
195 | 133k | file_path = pImpl->m_strFilePath; |
196 | 133k | OSL_ASSERT(!file_path.isEmpty()); |
197 | 133k | if (file_path.isEmpty()) |
198 | 0 | return osl_File_E_INVAL; |
199 | | |
200 | 133k | pStat->uValidFields = 0; |
201 | 133k | return osl_File_E_None; |
202 | 133k | } |
203 | | |
204 | | } |
205 | | |
206 | | oslFileError SAL_CALL osl_getFileStatus(oslDirectoryItem Item, oslFileStatus* pStat, sal_uInt32 uFieldMask) |
207 | 133k | { |
208 | 133k | DirectoryItem_Impl * pImpl = static_cast< DirectoryItem_Impl* >(Item); |
209 | | |
210 | 133k | OString file_path; |
211 | 133k | oslFileError osl_error = setup_osl_getFileStatus(pImpl, pStat, file_path); |
212 | 133k | if (osl_error != osl_File_E_None) |
213 | 0 | return osl_error; |
214 | | |
215 | 133k | struct stat file_stat; |
216 | | |
217 | 133k | bool bStatNeeded = is_stat_call_necessary(uFieldMask, pImpl->getFileType()); |
218 | 133k | if (bStatNeeded && (osl::lstat(file_path, file_stat) != 0)) |
219 | 0 | return oslTranslateFileError(errno); |
220 | | |
221 | 133k | if (bStatNeeded) |
222 | 133k | { |
223 | | // we set all these attributes because it's cheap |
224 | 133k | set_file_type(file_stat, pStat); |
225 | 133k | set_file_access_time(file_stat, pStat); |
226 | 133k | set_file_modify_time(file_stat, pStat); |
227 | 133k | set_file_size(file_stat, pStat); |
228 | 133k | set_file_attributes(file_path, file_stat, uFieldMask, pStat); |
229 | | |
230 | | // file exists semantic of osl_FileStatus_Mask_Validate |
231 | 133k | if ((uFieldMask & osl_FileStatus_Mask_LinkTargetURL) && S_ISLNK(file_stat.st_mode)) |
232 | 0 | { |
233 | 0 | osl_error = set_link_target_url(file_path, pStat); |
234 | 0 | if (osl_error != osl_File_E_None) |
235 | 0 | return osl_error; |
236 | 0 | } |
237 | 133k | } |
238 | 205 | #ifdef _DIRENT_HAVE_D_TYPE |
239 | 205 | else if (uFieldMask & osl_FileStatus_Mask_Type) |
240 | 0 | { |
241 | 0 | pStat->eType = pImpl->getFileType(); |
242 | 0 | pStat->uValidFields |= osl_FileStatus_Mask_Type; |
243 | 0 | } |
244 | 133k | #endif /* _DIRENT_HAVE_D_TYPE */ |
245 | | |
246 | 133k | if (uFieldMask & osl_FileStatus_Mask_FileURL) |
247 | 96.7k | { |
248 | 96.7k | OUString url; |
249 | 96.7k | if ((osl_error = osl::detail::convertPathnameToUrl(file_path, &url)) != osl_File_E_None) |
250 | 0 | return osl_error; |
251 | 96.7k | rtl_uString_assign(&pStat->ustrFileURL, url.pData); |
252 | | |
253 | 96.7k | pStat->uValidFields |= osl_FileStatus_Mask_FileURL; |
254 | 96.7k | } |
255 | | |
256 | 133k | if (uFieldMask & osl_FileStatus_Mask_FileName) |
257 | 96.5k | { |
258 | 96.5k | OString name; |
259 | 96.5k | osl_systemPathGetFileNameOrLastDirectoryPart(file_path.pData, &name.pData); |
260 | 96.5k | bool ok = rtl_convertStringToUString( |
261 | 96.5k | &pStat->ustrFileName, name.getStr(), name.getLength(), osl_getThreadTextEncoding(), |
262 | 96.5k | (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_DEFAULT | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_DEFAULT |
263 | 96.5k | | RTL_TEXTTOUNICODE_FLAGS_INVALID_DEFAULT)); |
264 | 96.5k | assert(ok); (void)ok; |
265 | 96.5k | pStat->uValidFields |= osl_FileStatus_Mask_FileName; |
266 | 96.5k | } |
267 | 133k | return osl_File_E_None; |
268 | 133k | } |
269 | | |
270 | | static oslFileError osl_psz_setFileAttributes( const char* pszFilePath, sal_uInt64 uAttributes ) |
271 | 0 | { |
272 | 0 | oslFileError osl_error = osl_File_E_None; |
273 | 0 | mode_t nNewMode = 0; |
274 | |
|
275 | 0 | OSL_ENSURE(!(osl_File_Attribute_Hidden & uAttributes), "osl_File_Attribute_Hidden doesn't work under Unix"); |
276 | |
|
277 | 0 | if (isForbidden(pszFilePath, osl_File_OpenFlag_Write)) |
278 | 0 | return osl_File_E_ACCES; |
279 | | |
280 | 0 | if (uAttributes & osl_File_Attribute_OwnRead) |
281 | 0 | nNewMode |= S_IRUSR; |
282 | |
|
283 | 0 | if (uAttributes & osl_File_Attribute_OwnWrite) |
284 | 0 | nNewMode|=S_IWUSR; |
285 | |
|
286 | 0 | if (uAttributes & osl_File_Attribute_OwnExe) |
287 | 0 | nNewMode|=S_IXUSR; |
288 | |
|
289 | 0 | if (uAttributes & osl_File_Attribute_GrpRead) |
290 | 0 | nNewMode|=S_IRGRP; |
291 | |
|
292 | 0 | if (uAttributes & osl_File_Attribute_GrpWrite) |
293 | 0 | nNewMode|=S_IWGRP; |
294 | |
|
295 | 0 | if (uAttributes & osl_File_Attribute_GrpExe) |
296 | 0 | nNewMode|=S_IXGRP; |
297 | |
|
298 | 0 | if (uAttributes & osl_File_Attribute_OthRead) |
299 | 0 | nNewMode|=S_IROTH; |
300 | |
|
301 | 0 | if (uAttributes & osl_File_Attribute_OthWrite) |
302 | 0 | nNewMode|=S_IWOTH; |
303 | |
|
304 | 0 | if (uAttributes & osl_File_Attribute_OthExe) |
305 | 0 | nNewMode|=S_IXOTH; |
306 | |
|
307 | 0 | if (chmod(pszFilePath, nNewMode) < 0) |
308 | 0 | osl_error = oslTranslateFileError(errno); |
309 | |
|
310 | 0 | return osl_error; |
311 | 0 | } |
312 | | |
313 | | oslFileError SAL_CALL osl_setFileAttributes( rtl_uString* ustrFileURL, sal_uInt64 uAttributes ) |
314 | 0 | { |
315 | 0 | char path[PATH_MAX]; |
316 | 0 | oslFileError eRet; |
317 | |
|
318 | 0 | OSL_ASSERT( ustrFileURL ); |
319 | | |
320 | | /* convert file url to system path */ |
321 | 0 | eRet = FileURLToPath( path, PATH_MAX, ustrFileURL ); |
322 | 0 | if( eRet != osl_File_E_None ) |
323 | 0 | return eRet; |
324 | | |
325 | | #ifdef MACOSX |
326 | | if ( macxp_resolveAlias( path, PATH_MAX ) != 0 ) |
327 | | return oslTranslateFileError( errno ); |
328 | | #endif/* MACOSX */ |
329 | | |
330 | 0 | return osl_psz_setFileAttributes( path, uAttributes ); |
331 | 0 | } |
332 | | |
333 | | static oslFileError osl_psz_setFileTime ( |
334 | | const char* pszFilePath, |
335 | | const TimeValue* pLastAccessTime, |
336 | | const TimeValue* pLastWriteTime ) |
337 | 0 | { |
338 | 0 | int nRet=0; |
339 | 0 | struct utimbuf aTimeBuffer; |
340 | 0 | struct stat aFileStat; |
341 | | #ifdef DEBUG_OSL_FILE |
342 | | struct tm* pTM=0; |
343 | | #endif |
344 | |
|
345 | 0 | if (isForbidden(pszFilePath, osl_File_OpenFlag_Write)) |
346 | 0 | return osl_File_E_ACCES; |
347 | | |
348 | 0 | nRet = lstat_c(pszFilePath,&aFileStat); |
349 | |
|
350 | 0 | if ( nRet < 0 ) |
351 | 0 | { |
352 | 0 | nRet=errno; |
353 | 0 | return oslTranslateFileError(nRet); |
354 | 0 | } |
355 | | |
356 | | #ifdef DEBUG_OSL_FILE |
357 | | fprintf(stderr,"File Times are (in localtime):\n"); |
358 | | pTM=localtime(&aFileStat.st_ctime); |
359 | | fprintf(stderr,"CreationTime is '%s'\n",asctime(pTM)); |
360 | | pTM=localtime(&aFileStat.st_atime); |
361 | | fprintf(stderr,"AccessTime is '%s'\n",asctime(pTM)); |
362 | | pTM=localtime(&aFileStat.st_mtime); |
363 | | fprintf(stderr,"Modification is '%s'\n",asctime(pTM)); |
364 | | |
365 | | fprintf(stderr,"File Times are (in UTC):\n"); |
366 | | fprintf(stderr,"CreationTime is '%s'\n",ctime(&aFileStat.st_ctime)); |
367 | | fprintf(stderr,"AccessTime is '%s'\n",ctime(&aTimeBuffer.actime)); |
368 | | fprintf(stderr,"Modification is '%s'\n",ctime(&aTimeBuffer.modtime)); |
369 | | #endif |
370 | | |
371 | 0 | if ( pLastAccessTime != nullptr ) |
372 | 0 | { |
373 | 0 | aTimeBuffer.actime=pLastAccessTime->Seconds; |
374 | 0 | } |
375 | 0 | else |
376 | 0 | { |
377 | 0 | aTimeBuffer.actime=aFileStat.st_atime; |
378 | 0 | } |
379 | |
|
380 | 0 | if ( pLastWriteTime != nullptr ) |
381 | 0 | { |
382 | 0 | aTimeBuffer.modtime=pLastWriteTime->Seconds; |
383 | 0 | } |
384 | 0 | else |
385 | 0 | { |
386 | 0 | aTimeBuffer.modtime=aFileStat.st_mtime; |
387 | 0 | } |
388 | | |
389 | | /* mfe: Creation time not used here! */ |
390 | |
|
391 | | #ifdef DEBUG_OSL_FILE |
392 | | fprintf(stderr,"File Times are (in localtime):\n"); |
393 | | pTM=localtime(&aFileStat.st_ctime); |
394 | | fprintf(stderr,"CreationTime now '%s'\n",asctime(pTM)); |
395 | | pTM=localtime(&aTimeBuffer.actime); |
396 | | fprintf(stderr,"AccessTime now '%s'\n",asctime(pTM)); |
397 | | pTM=localtime(&aTimeBuffer.modtime); |
398 | | fprintf(stderr,"Modification now '%s'\n",asctime(pTM)); |
399 | | |
400 | | fprintf(stderr,"File Times are (in UTC):\n"); |
401 | | fprintf(stderr,"CreationTime now '%s'\n",ctime(&aFileStat.st_ctime)); |
402 | | fprintf(stderr,"AccessTime now '%s'\n",ctime(&aTimeBuffer.actime)); |
403 | | fprintf(stderr,"Modification now '%s'\n",ctime(&aTimeBuffer.modtime)); |
404 | | #endif |
405 | |
|
406 | 0 | nRet = utime_c(pszFilePath,&aTimeBuffer); |
407 | 0 | if ( nRet < 0 ) |
408 | 0 | { |
409 | 0 | nRet=errno; |
410 | 0 | return oslTranslateFileError(nRet); |
411 | 0 | } |
412 | | |
413 | 0 | return osl_File_E_None; |
414 | 0 | } |
415 | | |
416 | | oslFileError SAL_CALL osl_setFileTime ( |
417 | | rtl_uString* ustrFileURL, |
418 | | SAL_UNUSED_PARAMETER const TimeValue* /* pCreationTime */, |
419 | | const TimeValue* pLastAccessTime, |
420 | | const TimeValue* pLastWriteTime ) |
421 | 0 | { |
422 | 0 | char path[PATH_MAX]; |
423 | 0 | oslFileError eRet; |
424 | |
|
425 | 0 | OSL_ASSERT( ustrFileURL ); |
426 | | |
427 | | /* convert file url to system path */ |
428 | 0 | eRet = FileURLToPath( path, PATH_MAX, ustrFileURL ); |
429 | 0 | if( eRet != osl_File_E_None ) |
430 | 0 | return eRet; |
431 | | |
432 | | #ifdef MACOSX |
433 | | if ( macxp_resolveAlias( path, PATH_MAX ) != 0 ) |
434 | | return oslTranslateFileError( errno ); |
435 | | #endif/* MACOSX */ |
436 | | |
437 | 0 | return osl_psz_setFileTime( path, pLastAccessTime, pLastWriteTime ); |
438 | 0 | } |
439 | | |
440 | | sal_Bool |
441 | | SAL_CALL osl_identicalDirectoryItem( oslDirectoryItem a, oslDirectoryItem b) |
442 | 0 | { |
443 | 0 | DirectoryItem_Impl *pA = static_cast<DirectoryItem_Impl *>(a); |
444 | 0 | DirectoryItem_Impl *pB = static_cast<DirectoryItem_Impl *>(b); |
445 | 0 | if (a == b) |
446 | 0 | return true; |
447 | | /* same name => same item, unless renaming / moving madness has occurred */ |
448 | 0 | if (pA->m_strFilePath == pB->m_strFilePath) |
449 | 0 | return true; |
450 | | |
451 | 0 | struct stat a_stat, b_stat; |
452 | |
|
453 | 0 | if (osl::lstat(pA->m_strFilePath, a_stat) != 0 || |
454 | 0 | osl::lstat(pB->m_strFilePath, b_stat) != 0) |
455 | 0 | return false; |
456 | | |
457 | 0 | return (a_stat.st_ino == b_stat.st_ino); |
458 | 0 | } |
459 | | |
460 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |