| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e97daea commit 4e1edae
20 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -419,7 +419,7 @@ static_library("minizip") { | |||
| 419 | 419 | ] | |
| 420 | 420 | } | |
| 421 | 421 | ||
| 422 | - if (is_apple || is_android || is_nacl) { | ||
| 422 | + if (is_apple || is_android) { | ||
| 423 | 423 | # Mac, Android and the BSDs don't have fopen64, ftello64, or fseeko64. We | |
| 424 | 424 | # use fopen, ftell, and fseek instead on these systems. | |
| 425 | 425 | defines = [ "USE_FILE32API" ] | |
@@ -551,6 +551,7 @@ if (build_with_chromium) { | |||
| 551 | 551 | "google:compression_utils", | |
| 552 | 552 | "google:zip", | |
| 553 | 553 | "//base/test:test_support", | |
| 554 | + "//crypto", | ||
| 554 | 555 | "//testing/gtest", | |
| 555 | 556 | ] | |
| 556 | 557 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,8 +1,9 @@ | |||
| 1 | 1 | Name: zlib | |
| 2 | 2 | Short Name: zlib | |
| 3 | - URL: http://zlib.net/ | ||
| 3 | + URL: https://github.com/madler/zlib | ||
| 4 | 4 | Version: 1.3.1 | |
| 5 | 5 | Revision: 51b7f2abdade71cd9bb0e7a373ef2610ec6f9daf | |
| 6 | + Update Mechanism: Manual (https://crbug.com/422348588) | ||
| 6 | 7 | CPEPrefix: cpe:/a:zlib:zlib:1.3.1 | |
| 7 | 8 | Security Critical: yes | |
| 8 | 9 | Shipped: yes | |
@@ -19,8 +20,8 @@ library. zlib implements the "deflate" compression algorithm described by RFC | |||
| 19 | 20 | also implements the zlib (RFC 1950) and gzip (RFC 1952) wrapper formats. | |
| 20 | 21 | ||
| 21 | 22 | Local Modifications: | |
| 22 | - - Only source code from the zlib distribution used to build the zlib and | ||
| 23 | - minizip libraries are present. Many other files have been omitted. Only *.c | ||
| 23 | + - Only source code from the http://zlib.net distribution used to build the zlib | ||
| 24 | + and minizip libraries are present. Many other files have been omitted. Only *.c | ||
| 24 | 25 | and *.h files from the upstream root directory, contrib/minizip and | |
| 25 | 26 | examples/zpipe.c were imported. | |
| 26 | 27 | - The files named '*simd*' are original x86/Arm/RISC-V specific optimizations. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,6 +53,9 @@ | |||
| 53 | 53 | ||
| 54 | 54 | #include <tmmintrin.h> | |
| 55 | 55 | ||
| 56 | + #if defined(__GNUC__) | ||
| 57 | + __attribute__((__target__("ssse3"))) | ||
| 58 | + #endif | ||
| 56 | 59 | uint32_t ZLIB_INTERNAL adler32_simd_( /* SSSE3 */ | |
| 57 | 60 | uint32_t adler, | |
| 58 | 61 | const unsigned char *buf, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,6 +3,7 @@ Short Name: minizip | |||
| 3 | 3 | URL: https://github.com/madler/zlib/tree/master/contrib/minizip | |
| 4 | 4 | Version: 1.3.1.1 | |
| 5 | 5 | Revision: ef24c4c7502169f016dcd2a26923dbaf3216748c | |
| 6 | + Update Mechanism: Manual | ||
| 6 | 7 | License: Zlib | |
| 7 | 8 | License File: //third_party/zlib/LICENSE | |
| 8 | 9 | Shipped: yes | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -64,6 +64,7 @@ | |||
| 64 | 64 | */ | |
| 65 | 65 | ||
| 66 | 66 | ||
| 67 | + #include <stdint.h> | ||
| 67 | 68 | #include <stdio.h> | |
| 68 | 69 | #include <stdlib.h> | |
| 69 | 70 | #include <string.h> | |
@@ -837,6 +838,7 @@ local int unz64local_GetCurrentFileInfoInternal(unzFile file, | |||
| 837 | 838 | uLong uMagic; | |
| 838 | 839 | long lSeek=0; | |
| 839 | 840 | uLong uL; | |
| 841 | + uLong uFileNameCrc; | ||
| 840 | 842 | ||
| 841 | 843 | if (file==NULL) | |
| 842 | 844 | return UNZ_PARAMERROR; | |
@@ -908,21 +910,34 @@ local int unz64local_GetCurrentFileInfoInternal(unzFile file, | |||
| 908 | 910 | file_info_internal.offset_curfile = uL; | |
| 909 | 911 | ||
| 910 | 912 | lSeek+=file_info.size_filename; | |
| 911 | - if ((err==UNZ_OK) && (szFileName!=NULL)) | ||
| 913 | + if (err==UNZ_OK) | ||
| 912 | 914 | { | |
| 913 | - uLong uSizeRead ; | ||
| 914 | - if (file_info.size_filename<fileNameBufferSize) | ||
| 915 | + char szCurrentFileName[UINT16_MAX] = {0}; | ||
| 916 | + | ||
| 917 | + if (file_info.size_filename > 0) | ||
| 915 | 918 | { | |
| 916 | - *(szFileName+file_info.size_filename)='\0'; | ||
| 917 | - uSizeRead = file_info.size_filename; | ||
| 919 | + if (ZREAD64(s->z_filefunc, s->filestream, szCurrentFileName, file_info.size_filename) != file_info.size_filename) | ||
| 920 | + { | ||
| 921 | + err=UNZ_ERRNO; | ||
| 922 | + } | ||
| 918 | 923 | } | |
| 919 | - else | ||
| 920 | - uSizeRead = fileNameBufferSize; | ||
| 921 | 924 | ||
| 922 | - if ((file_info.size_filename>0) && (fileNameBufferSize>0)) | ||
| 923 | - if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead) | ||
| 924 | - err=UNZ_ERRNO; | ||
| 925 | - lSeek -= uSizeRead; | ||
| 925 | + uFileNameCrc = crc32(0, (unsigned char*)szCurrentFileName, file_info.size_filename); | ||
| 926 | + | ||
| 927 | + if (szFileName != NULL) | ||
| 928 | + { | ||
| 929 | + if (fileNameBufferSize <= file_info.size_filename) | ||
| 930 | + { | ||
| 931 | + memcpy(szFileName, szCurrentFileName, fileNameBufferSize); | ||
| 932 | + } | ||
| 933 | + else | ||
| 934 | + { | ||
| 935 | + memcpy(szFileName, szCurrentFileName, file_info.size_filename); | ||
| 936 | + szFileName[file_info.size_filename] = '\0'; | ||
| 937 | + } | ||
| 938 | + } | ||
| 939 | + | ||
| 940 | + lSeek -= file_info.size_filename; | ||
| 926 | 941 | } | |
| 927 | 942 | ||
| 928 | 943 | // Read extrafield | |
@@ -1012,7 +1027,15 @@ local int unz64local_GetCurrentFileInfoInternal(unzFile file, | |||
| 1012 | 1027 | { | |
| 1013 | 1028 | int version = 0; | |
| 1014 | 1029 | ||
| 1015 | - if (unz64local_getByte(&s->z_filefunc, s->filestream, &version) != UNZ_OK) | ||
| 1030 | + if (dataSize < 1 + 4) | ||
| 1031 | + { | ||
| 1032 | + /* dataSize includes version (1 byte), uCrc (4 bytes), and | ||
| 1033 | + * the filename data. If it's too small, fileNameSize below | ||
| 1034 | + * would overflow. */ | ||
| 1035 | + err = UNZ_ERRNO; | ||
| 1036 | + break; | ||
| 1037 | + } | ||
| 1038 | + else if (unz64local_getByte(&s->z_filefunc, s->filestream, &version) != UNZ_OK) | ||
| 1016 | 1039 | { | |
| 1017 | 1040 | err = UNZ_ERRNO; | |
| 1018 | 1041 | } | |
@@ -1025,16 +1048,16 @@ local int unz64local_GetCurrentFileInfoInternal(unzFile file, | |||
| 1025 | 1048 | } | |
| 1026 | 1049 | else | |
| 1027 | 1050 | { | |
| 1028 | - uLong uCrc, uHeaderCrc, fileNameSize; | ||
| 1051 | + uLong uCrc, fileNameSize; | ||
| 1029 | 1052 | ||
| 1030 | 1053 | if (unz64local_getLong(&s->z_filefunc, s->filestream, &uCrc) != UNZ_OK) | |
| 1031 | 1054 | { | |
| 1032 | 1055 | err = UNZ_ERRNO; | |
| 1033 | 1056 | } | |
| 1034 | - uHeaderCrc = crc32(0, (const unsigned char *)szFileName, file_info.size_filename); | ||
| 1035 | - fileNameSize = dataSize - (2 * sizeof (short) + 1); | ||
| 1057 | + fileNameSize = dataSize - (1 + 4); /* 1 for version, 4 for uCrc */ | ||
| 1058 | + | ||
| 1036 | 1059 | /* Check CRC against file name in the header. */ | |
| 1037 | - if (uHeaderCrc != uCrc) | ||
| 1060 | + if (uCrc != uFileNameCrc) | ||
| 1038 | 1061 | { | |
| 1039 | 1062 | if (ZSEEK64(s->z_filefunc, s->filestream, fileNameSize, ZLIB_FILEFUNC_SEEK_CUR) != 0) | |
| 1040 | 1063 | { | |
@@ -1043,24 +1066,28 @@ local int unz64local_GetCurrentFileInfoInternal(unzFile file, | |||
| 1043 | 1066 | } | |
| 1044 | 1067 | else | |
| 1045 | 1068 | { | |
| 1046 | - uLong uSizeRead; | ||
| 1047 | - | ||
| 1048 | 1069 | file_info.size_filename = fileNameSize; | |
| 1049 | 1070 | ||
| 1050 | - if (fileNameSize < fileNameBufferSize) | ||
| 1051 | - { | ||
| 1052 | - *(szFileName + fileNameSize) = '\0'; | ||
| 1053 | - uSizeRead = fileNameSize; | ||
| 1054 | - } | ||
| 1055 | - else | ||
| 1071 | + char szCurrentFileName[UINT16_MAX] = {0}; | ||
| 1072 | + | ||
| 1073 | + if (file_info.size_filename > 0) | ||
| 1056 | 1074 | { | |
| 1057 | - uSizeRead = fileNameBufferSize; | ||
| 1075 | + if (ZREAD64(s->z_filefunc, s->filestream, szCurrentFileName, file_info.size_filename) != file_info.size_filename) | ||
| 1076 | + { | ||
| 1077 | + err = UNZ_ERRNO; | ||
| 1078 | + } | ||
| 1058 | 1079 | } | |
| 1059 | - if ((fileNameSize > 0) && (fileNameBufferSize > 0)) | ||
| 1080 | + | ||
| 1081 | + if (szFileName != NULL) | ||
| 1060 | 1082 | { | |
| 1061 | - if (ZREAD64(s->z_filefunc, s->filestream, szFileName, uSizeRead) != uSizeRead) | ||
| 1083 | + if (fileNameBufferSize <= file_info.size_filename) | ||
| 1062 | 1084 | { | |
| 1063 | - err = UNZ_ERRNO; | ||
| 1085 | + memcpy(szFileName, szCurrentFileName, fileNameBufferSize); | ||
| 1086 | + } | ||
| 1087 | + else | ||
| 1088 | + { | ||
| 1089 | + memcpy(szFileName, szCurrentFileName, file_info.size_filename); | ||
| 1090 | + szFileName[file_info.size_filename] = '\0'; | ||
| 1064 | 1091 | } | |
| 1065 | 1092 | } | |
| 1066 | 1093 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,9 +2,11 @@ | |||
| 2 | 2 | // Use of this source code is governed by a BSD-style license that can be | |
| 3 | 3 | // found in the LICENSE file. | |
| 4 | 4 | ||
| 5 | + #include <fuzzer/FuzzedDataProvider.h> | ||
| 5 | 6 | #include <algorithm> | |
| 6 | 7 | #include <cstdint> | |
| 7 | 8 | #include <cstring> | |
| 9 | + #include <memory> | ||
| 8 | 10 | #include <vector> | |
| 9 | 11 | ||
| 10 | 12 | #include "unzip.h" | |
@@ -19,11 +21,30 @@ | |||
| 19 | 21 | } while (0) | |
| 20 | 22 | ||
| 21 | 23 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { | |
| 24 | + FuzzedDataProvider fdp(data, size); | ||
| 25 | + | ||
| 26 | + unsigned long filename_sz = fdp.ConsumeIntegralInRange(0, UINT16_MAX + 3); | ||
| 27 | + unsigned long extra_sz = fdp.ConsumeIntegralInRange(0, UINT16_MAX + 3); | ||
| 28 | + unsigned long comment_sz = fdp.ConsumeIntegralInRange(0, UINT16_MAX + 3); | ||
| 29 | + | ||
| 30 | + std::unique_ptr<char[]> filename; | ||
| 31 | + if (fdp.ConsumeBool()) { | ||
| 32 | + filename = std::make_unique<char[]>(filename_sz); | ||
| 33 | + } | ||
| 34 | + std::unique_ptr<char[]> extra; | ||
| 35 | + if (fdp.ConsumeBool()) { | ||
| 36 | + extra = std::make_unique<char[]>(extra_sz); | ||
| 37 | + } | ||
| 38 | + std::unique_ptr<char[]> comment; | ||
| 39 | + if (fdp.ConsumeBool()) { | ||
| 40 | + comment = std::make_unique<char[]>(comment_sz); | ||
| 41 | + } | ||
| 42 | + | ||
| 22 | 43 | // Mock read-only filesystem with only one file, file_data. In the calls | |
| 23 | 44 | // below, 'opaque' points to file_data, and 'strm' points to the file's seek | |
| 24 | 45 | // position, which is heap allocated so that failing to "close" it triggers a | |
| 25 | 46 | // leak error. | |
| 26 | - std::vector<uint8_t> file_data(data, data + size); | ||
| 47 | + std::vector<uint8_t> file_data = fdp.ConsumeRemainingBytes<uint8_t>(); | ||
| 27 | 48 | zlib_filefunc64_def file_func = { | |
| 28 | 49 | .zopen64_file = [](void* opaque, const void* filename, | |
| 29 | 50 | int mode) -> void* { | |
@@ -83,19 +104,23 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { | |||
| 83 | 104 | while (true) { | |
| 84 | 105 | unz_file_info64 info = {0}; | |
| 85 | 106 | ||
| 86 | - // TODO: Pass nullptrs and different buffer sizes to cover more code. | ||
| 87 | - char filename[UINT16_MAX + 1]; // +1 for the null terminator. | ||
| 88 | - char extra[UINT16_MAX]; // No null terminator. | ||
| 89 | - char comment[UINT16_MAX + 1]; // +1 for the null terminator. | ||
| 90 | - | ||
| 91 | - if (unzGetCurrentFileInfo64(uzf, &info, filename, sizeof(filename), extra, | ||
| 92 | - sizeof(extra), comment, sizeof(comment)) == UNZ_OK) { | ||
| 93 | - ASSERT(info.size_filename <= UINT16_MAX); | ||
| 94 | - ASSERT(info.size_file_extra <= UINT16_MAX); | ||
| 95 | - ASSERT(info.size_file_comment <= UINT16_MAX); | ||
| 96 | - | ||
| 97 | - ASSERT(filename[info.size_filename] == '\0'); | ||
| 98 | - ASSERT(comment[info.size_file_comment] == '\0'); | ||
| 107 | + if (unzGetCurrentFileInfo64(uzf, &info, filename.get(), filename_sz, extra.get(), | ||
| 108 | + extra_sz, comment.get(), comment_sz) == UNZ_OK) { | ||
| 109 | + if (filename) { | ||
| 110 | + ASSERT(info.size_filename <= UINT16_MAX); | ||
| 111 | + if (info.size_filename < filename_sz) { | ||
| 112 | + ASSERT(filename[info.size_filename] == '\0'); | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + if (extra) { | ||
| 116 | + ASSERT(info.size_file_extra <= UINT16_MAX); | ||
| 117 | + } | ||
| 118 | + if (comment) { | ||
| 119 | + ASSERT(info.size_file_comment <= UINT16_MAX); | ||
| 120 | + if (info.size_file_comment < comment_sz) { | ||
| 121 | + ASSERT(comment[info.size_file_comment] == '\0'); | ||
| 122 | + } | ||
| 123 | + } | ||
| 99 | 124 | } | |
| 100 | 125 | ||
| 101 | 126 | if (unzOpenCurrentFile(uzf) == UNZ_OK) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,6 +13,7 @@ | |||
| 13 | 13 | #if !defined(CMAKE_STANDALONE_UNITTESTS) | |
| 14 | 14 | #include "base/files/file_path.h" | |
| 15 | 15 | #include "base/files/scoped_temp_dir.h" | |
| 16 | + #include "base/path_service.h" | ||
| 16 | 17 | ||
| 17 | 18 | #include "third_party/zlib/contrib/minizip/unzip.h" | |
| 18 | 19 | #include "third_party/zlib/contrib/minizip/zip.h" | |
@@ -1287,4 +1288,62 @@ TEST(ZlibTest, ZipExtraFieldSize) { | |||
| 1287 | 1288 | EXPECT_EQ(unzClose(uzf), UNZ_OK); | |
| 1288 | 1289 | } | |
| 1289 | 1290 | ||
| 1291 | + static base::FilePath TestDataDir() { | ||
| 1292 | + base::FilePath path; | ||
| 1293 | + bool success = base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &path); | ||
| 1294 | + EXPECT_TRUE(success); | ||
| 1295 | + return path | ||
| 1296 | + .AppendASCII("third_party") | ||
| 1297 | + .AppendASCII("zlib") | ||
| 1298 | + .AppendASCII("google") | ||
| 1299 | + .AppendASCII("test") | ||
| 1300 | + .AppendASCII("data"); | ||
| 1301 | + } | ||
| 1302 | + | ||
| 1303 | + TEST(ZlibTest, ZipUnicodePathExtraSizeFilenameOverflow) { | ||
| 1304 | + // This is based on components/test/data/unzip_service/bug953599.zip (added | ||
| 1305 | + // in https://crrev.com/1004132), with the Unicode Path Extra Field's | ||
| 1306 | + // dataSize hex edited to four. | ||
| 1307 | + base::FilePath zip_file = TestDataDir().AppendASCII("unicode_path_extra_overflow.zip"); | ||
| 1308 | + unzFile uzf = unzOpen(zip_file.AsUTF8Unsafe().c_str()); | ||
| 1309 | + ASSERT_NE(uzf, nullptr); | ||
| 1310 | + EXPECT_EQ(unzGoToFirstFile(uzf), UNZ_ERRNO); | ||
| 1311 | + EXPECT_EQ(unzClose(uzf), UNZ_OK); | ||
| 1312 | + } | ||
| 1313 | + | ||
| 1314 | + TEST(ZlibTest, ZipUnicodePathExtra) { | ||
| 1315 | + // This is components/test/data/unzip_service/bug953599.zip (added in | ||
| 1316 | + // https://crrev.com/1004132). | ||
| 1317 | + base::FilePath zip_file = TestDataDir().AppendASCII("unicode_path_extra.zip"); | ||
| 1318 | + unzFile uzf = unzOpen(zip_file.AsUTF8Unsafe().c_str()); | ||
| 1319 | + ASSERT_NE(uzf, nullptr); | ||
| 1320 | + | ||
| 1321 | + char long_buf[15], short_buf[3]; | ||
| 1322 | + unz_file_info file_info; | ||
| 1323 | + | ||
| 1324 | + ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); | ||
| 1325 | + ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, long_buf, sizeof(long_buf), | ||
| 1326 | + nullptr, 0, nullptr, 0), UNZ_OK); | ||
| 1327 | + ASSERT_EQ(file_info.size_filename, 14); | ||
| 1328 | + ASSERT_EQ(std::string(long_buf), "\xec\x83\x88 \xeb\xac\xb8\xec\x84\x9c.txt"); | ||
| 1329 | + | ||
| 1330 | + // Even if the file name buffer is too short to hold the whole filename, the | ||
| 1331 | + // unicode path extra field should get parsed correctly, size_filename set, | ||
| 1332 | + // and the file name buffer should receive the first bytes. | ||
| 1333 | + ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); | ||
| 1334 | + ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, short_buf, sizeof(short_buf), | ||
| 1335 | + nullptr, 0, nullptr, 0), UNZ_OK); | ||
| 1336 | + ASSERT_EQ(file_info.size_filename, 14); | ||
| 1337 | + ASSERT_EQ(std::string(short_buf, sizeof(short_buf)), "\xec\x83\x88"); | ||
| 1338 | + | ||
| 1339 | + // Also with a null filename buffer, the unicode path extra field should get | ||
| 1340 | + // parsed and size_filename set correctly. | ||
| 1341 | + ASSERT_EQ(unzGoToFirstFile(uzf), UNZ_OK); | ||
| 1342 | + ASSERT_EQ(unzGetCurrentFileInfo(uzf, &file_info, nullptr, 0, nullptr, 0, | ||
| 1343 | + nullptr, 0), UNZ_OK); | ||
| 1344 | + ASSERT_EQ(file_info.size_filename, 14); | ||
| 1345 | + | ||
| 1346 | + EXPECT_EQ(unzClose(uzf), UNZ_OK); | ||
| 1347 | + } | ||
| 1348 | + | ||
| 1290 | 1349 | #endif | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,6 +1,7 @@ | |||
| 1 | 1 | include_rules = [ | |
| 2 | 2 | '+base', | |
| 3 | 3 | '+build', | |
| 4 | + '+crypto', | ||
| 4 | 5 | '+testing', | |
| 5 | 6 | '+third_party/icu/source/i18n/unicode', | |
| 6 | 7 | '+third_party/zlib/zlib.h', | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,6 +16,10 @@ def make_file(zf, path, content): | |||
| 16 | 16 | zf.writestr(zipfile.ZipInfo(path), content) | |
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | + def make_dir(zf, path): | ||
| 20 | + zf.mkdir(path) | ||
| 21 | + | ||
| 22 | + | ||
| 19 | 23 | def make_test_zips(): | |
| 20 | 24 | with make_zip('symlinks.zip') as zf: | |
| 21 | 25 | make_file(zf, 'a.txt', 'A') | |
@@ -39,6 +43,11 @@ def make_test_zips(): | |||
| 39 | 43 | make_link(zf, 'file', 'link') | |
| 40 | 44 | make_file(zf, 'link', 'Hello world') | |
| 41 | 45 | ||
| 46 | + with make_zip('symlink_follow_own_link_dir.zip') as zf: | ||
| 47 | + make_dir(zf, 'dir') | ||
| 48 | + make_link(zf, 'dir', 'link') | ||
| 49 | + make_file(zf, 'link/file', 'Hello world') | ||
| 50 | + | ||
| 42 | 51 | with make_zip('symlink_duplicate_link.zip') as zf: | |
| 43 | 52 | make_link(zf, 'target_1', 'link') | |
| 44 | 53 | make_link(zf, 'target_2', 'link') | |
| Back | FazBrowse Home | New Git URL |
0 commit comments