| 731 | | public SocketConduit connectPassive() |
|---|
| 732 | | { |
|---|
| 733 | | Address connect_to = null; |
|---|
| 734 | | |
|---|
| 735 | | // SPSV, which is just a port number. |
|---|
| 736 | | if (this.is_supported("SPSV")) |
|---|
| 737 | | { |
|---|
| 738 | | this.sendCommand("SPSV"); |
|---|
| 739 | | auto response = this.readResponse("227"); |
|---|
| 740 | | |
|---|
| 741 | | // Connecting to the same host. |
|---|
| 742 | | IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); |
|---|
| 743 | | assert (remote !is null); |
|---|
| 744 | | |
|---|
| 745 | | uint address = remote.addr(); |
|---|
| 746 | | uint port = toInt(response.message); |
|---|
| 747 | | |
|---|
| 748 | | connect_to = new IPv4Address(address, cast(ushort) port); |
|---|
| 749 | | } |
|---|
| 750 | | // Extended passive mode (IP v6, etc.) |
|---|
| 751 | | else if (this.is_supported("EPSV")) |
|---|
| 752 | | { |
|---|
| 753 | | this.sendCommand("EPSV"); |
|---|
| 754 | | auto response = this.readResponse("229"); |
|---|
| 755 | | |
|---|
| 756 | | // Try to pull out the (possibly not parenthesized) address. |
|---|
| 757 | | auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); |
|---|
| 758 | | if ( !r.test(response.message[0 .. find(response.message, '\n') ])) |
|---|
| 759 | | throw new FtpException("CLIENT: Unable to parse address", "501"); |
|---|
| 760 | | |
|---|
| 761 | | IPv4Address remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); |
|---|
| 762 | | assert (remote !is null); |
|---|
| 763 | | |
|---|
| 764 | | uint address = remote.addr(); |
|---|
| 765 | | uint port = toInt(r.match(1)); |
|---|
| 766 | | |
|---|
| 767 | | connect_to = new IPv4Address(address, cast(ushort) port); |
|---|
| 768 | | } |
|---|
| 769 | | else |
|---|
| 770 | | { |
|---|
| 771 | | this.sendCommand("PASV"); |
|---|
| 772 | | auto response = this.readResponse("227"); |
|---|
| 773 | | |
|---|
| 774 | | // Try to pull out the (possibly not parenthesized) address. |
|---|
| 775 | | auto r = Regex(`(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); |
|---|
| 776 | | if ( !r.test(response.message[0 .. find(response.message, '\n') ]) ) |
|---|
| 777 | | throw new FtpException("CLIENT: Unable to parse address", "501"); |
|---|
| 778 | | |
|---|
| 779 | | // Now put it into something std.socket will understand. |
|---|
| 780 | | char[] address = r.match(1)~"."~r.match(2)~"."~r.match(3)~"."~r.match(4); |
|---|
| 781 | | uint port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt(r.match(7)) : 0); |
|---|
| 782 | | |
|---|
| 783 | | // Okay, we've got it! |
|---|
| 784 | | connect_to = new IPv4Address(address, port); |
|---|
| 785 | | } |
|---|
| 786 | | |
|---|
| 787 | | scope (exit) |
|---|
| 788 | | delete connect_to; |
|---|
| 789 | | |
|---|
| 790 | | // This will throw an exception if it cannot connect. |
|---|
| 791 | | SocketConduit sock = new SocketConduit(); |
|---|
| | 733 | public SocketConduit connectPassive() { |
|---|
| | 734 | Address connect_to = null; |
|---|
| | 735 | |
|---|
| | 736 | // SPSV, which is just a port number. |
|---|
| | 737 | if(this.is_supported("SPSV")) { |
|---|
| | 738 | this.sendCommand("SPSV"); |
|---|
| | 739 | auto response = this.readResponse("227"); |
|---|
| | 740 | |
|---|
| | 741 | // Connecting to the same host. |
|---|
| | 742 | IPv4Address |
|---|
| | 743 | remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); |
|---|
| | 744 | assert(remote !is null); |
|---|
| | 745 | |
|---|
| | 746 | uint address = remote.addr(); |
|---|
| | 747 | uint port = toInt(response.message); |
|---|
| | 748 | |
|---|
| | 749 | connect_to = new IPv4Address(address, cast(ushort) port); |
|---|
| | 750 | } |
|---|
| | 751 | // Extended passive mode (IP v6, etc.) |
|---|
| | 752 | else if(this.is_supported("EPSV")) { |
|---|
| | 753 | this.sendCommand("EPSV"); |
|---|
| | 754 | auto response = this.readResponse("229"); |
|---|
| | 755 | |
|---|
| | 756 | // Try to pull out the (possibly not parenthesized) address. |
|---|
| | 757 | auto r = Regex(`\([^0-9][^0-9][^0-9](\d+)[^0-9]\)`); |
|---|
| | 758 | if(!r.test(response.message[0 .. find(response.message, '\n')])) |
|---|
| | 759 | throw new FtpException("CLIENT: Unable to parse address", "501"); |
|---|
| | 760 | |
|---|
| | 761 | IPv4Address |
|---|
| | 762 | remote = cast(IPv4Address) this.socket_.socket.remoteAddress(); |
|---|
| | 763 | assert(remote !is null); |
|---|
| | 764 | |
|---|
| | 765 | uint address = remote.addr(); |
|---|
| | 766 | uint port = toInt(r.match(1)); |
|---|
| | 767 | |
|---|
| | 768 | connect_to = new IPv4Address(address, cast(ushort) port); |
|---|
| | 769 | } else { |
|---|
| | 770 | this.sendCommand("PASV"); |
|---|
| | 771 | auto response = this.readResponse("227"); |
|---|
| | 772 | |
|---|
| | 773 | // Try to pull out the (possibly not parenthesized) address. |
|---|
| | 774 | auto r = Regex( |
|---|
| | 775 | `(\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(,\s*(\d+))?`); |
|---|
| | 776 | if(!r.test(response.message[0 .. find(response.message, '\n')])) |
|---|
| | 777 | throw new FtpException("CLIENT: Unable to parse address", "501"); |
|---|
| | 778 | |
|---|
| | 779 | // Now put it into something std.socket will understand. |
|---|
| | 780 | char[] |
|---|
| | 781 | address = r.match(1) ~ "." ~ r.match(2) ~ "." ~ r.match(3) ~ "." ~ r.match( |
|---|
| | 782 | 4); |
|---|
| | 783 | uint |
|---|
| | 784 | port = (toInt(r.match(5)) << 8) + (r.match(7).length > 0 ? toInt( |
|---|
| | 785 | r.match(7)) : 0); |
|---|
| | 786 | |
|---|
| | 787 | // Okay, we've got it! |
|---|
| | 788 | connect_to = new IPv4Address(address, port); |
|---|
| | 789 | } |
|---|
| | 790 | |
|---|
| | 791 | scope(exit) |
|---|
| | 792 | delete connect_to; |
|---|
| | 793 | |
|---|
| | 794 | // This will throw an exception if it cannot connect. |
|---|
| | 795 | SocketConduit sock = new SocketConduit(); |
|---|
| 876 | | public void finishDataCommand(SocketConduit data) |
|---|
| 877 | | { |
|---|
| 878 | | // Close the socket. This tells the server we're done (EOF.) |
|---|
| 879 | | data.close(); |
|---|
| 880 | | data.detach(); |
|---|
| 881 | | |
|---|
| 882 | | // We shouldn't get a 250 in STREAM mode. |
|---|
| 883 | | this.readResponse("226"); |
|---|
| 884 | | } |
|---|
| 885 | | |
|---|
| 886 | | public SocketConduit processDataCommand(char[] command, char[][] parameters ...) |
|---|
| 887 | | { |
|---|
| 888 | | // Create a connection. |
|---|
| 889 | | SocketConduit data = this.getDataSocket(); |
|---|
| 890 | | scope (failure) |
|---|
| 891 | | { |
|---|
| 892 | | // Close the socket, whether we were listening or not. |
|---|
| 893 | | data.close(); |
|---|
| 894 | | } |
|---|
| 895 | | |
|---|
| 896 | | // Tell the server about it. |
|---|
| 897 | | this.sendCommand(command, parameters); |
|---|
| 898 | | |
|---|
| 899 | | // We should always get a 150/125 response. |
|---|
| 900 | | auto response = this.readResponse(); |
|---|
| 901 | | if (response.code != "150" && response.code != "125") |
|---|
| 902 | | exception (response); |
|---|
| 903 | | |
|---|
| 904 | | // We might need to do this for active connections. |
|---|
| 905 | | this.prepareDataSocket(data); |
|---|
| 906 | | |
|---|
| 907 | | return data; |
|---|
| 908 | | } |
|---|
| 909 | | |
|---|
| 910 | | public FtpFileInfo[] ls(char[] path = "") // default to current dir |
|---|
| 911 | | in |
|---|
| 912 | | { |
|---|
| 913 | | assert (path.length == 0 || path[path.length - 1] != '/'); |
|---|
| 914 | | } |
|---|
| 915 | | body |
|---|
| 916 | | { |
|---|
| 917 | | FtpFileInfo[] dir; |
|---|
| 918 | | |
|---|
| 919 | | // We'll try MLSD (which is so much better) first... but it may fail. |
|---|
| 920 | | bool mlsd_success = false; |
|---|
| 921 | | SocketConduit data = null; |
|---|
| 922 | | |
|---|
| 923 | | // Try it if it could/might/maybe is supported. |
|---|
| 924 | | if (this.isSupported("MLST")) |
|---|
| 925 | | { |
|---|
| 926 | | mlsd_success = true; |
|---|
| 927 | | |
|---|
| 928 | | // Since this is a data command, processDataCommand handles |
|---|
| 929 | | // checking the response... just catch its Exception. |
|---|
| 930 | | try |
|---|
| 931 | | { |
|---|
| 932 | | if (path.length > 0) |
|---|
| 933 | | data = this.processDataCommand("MLSD", path); |
|---|
| 934 | | else |
|---|
| 935 | | data = this.processDataCommand("MLSD"); |
|---|
| 936 | | } |
|---|
| 937 | | catch (FtpException) |
|---|
| 938 | | mlsd_success = false; |
|---|
| 939 | | } |
|---|
| 940 | | |
|---|
| 941 | | // If it passed, parse away! |
|---|
| 942 | | if (mlsd_success) |
|---|
| 943 | | { |
|---|
| 944 | | auto listing = new GrowBuffer; |
|---|
| 945 | | this.readStream(data, listing); |
|---|
| 946 | | this.finishDataCommand(data); |
|---|
| 947 | | |
|---|
| 948 | | // Each line is something in that directory. |
|---|
| 949 | | char[][] lines = Text.splitLines (cast(char[]) listing.slice()); |
|---|
| 950 | | scope (exit) |
|---|
| 951 | | delete lines; |
|---|
| 952 | | |
|---|
| 953 | | foreach (char[] line; lines) |
|---|
| 954 | | { |
|---|
| 955 | | // Parse each line exactly like MLST does. |
|---|
| 956 | | try |
|---|
| 957 | | { |
|---|
| 958 | | FtpFileInfo info = this.parseMlstLine(line); |
|---|
| 959 | | if (info.name.length > 0) |
|---|
| 960 | | dir ~= info; |
|---|
| 961 | | } |
|---|
| 962 | | catch(FtpException) |
|---|
| 963 | | { |
|---|
| 964 | | return this.sendListCommand(path); |
|---|
| 965 | | } |
|---|
| 966 | | } |
|---|
| 967 | | |
|---|
| 968 | | return dir; |
|---|
| 969 | | } |
|---|
| 970 | | // Fall back to LIST. |
|---|
| 971 | | else |
|---|
| 972 | | return this.sendListCommand(path); |
|---|
| 973 | | } |
|---|
| 974 | | |
|---|
| 975 | | protected void readStream(SocketConduit data, OutputStream stream, FtpProgress progress = null) |
|---|
| 976 | | in |
|---|
| 977 | | { |
|---|
| 978 | | assert (data !is null); |
|---|
| 979 | | assert (stream !is null); |
|---|
| 980 | | } |
|---|
| 981 | | body |
|---|
| 982 | | { |
|---|
| 983 | | // Set up a SocketSet so we can use select() - it's pretty efficient. |
|---|
| 984 | | SocketSet set = new SocketSet(); |
|---|
| 985 | | scope (exit) |
|---|
| 986 | | delete set; |
|---|
| 987 | | |
|---|
| 988 | | // At end_time, we bail. |
|---|
| 989 | | Time end_time = Clock.now + this.timeout; |
|---|
| 990 | | |
|---|
| 991 | | // This is the buffer the stream data is stored in. |
|---|
| 992 | | ubyte[8 * 1024] buf; |
|---|
| 993 | | int buf_size = 0; |
|---|
| 994 | | |
|---|
| 995 | | bool completed = false; |
|---|
| 996 | | size_t pos; |
|---|
| 997 | | while (Clock.now < end_time) |
|---|
| 998 | | { |
|---|
| 999 | | set.reset(); |
|---|
| 1000 | | set.add(data.socket); |
|---|
| 1001 | | |
|---|
| 1002 | | // Can we read yet, can we read yet? |
|---|
| 1003 | | int code = Socket.select(set, null, null, this.timeout); |
|---|
| 1004 | | if (code == -1 || code == 0) |
|---|
| 1005 | | break; |
|---|
| 1006 | | |
|---|
| 1007 | | buf_size = data.socket.receive(buf); |
|---|
| 1008 | | if (buf_size == data.socket.ERROR) |
|---|
| 1009 | | break; |
|---|
| 1010 | | |
|---|
| 1011 | | if (buf_size == 0) |
|---|
| 1012 | | { |
|---|
| 1013 | | completed = true; |
|---|
| 1014 | | break; |
|---|
| 1015 | | } |
|---|
| 1016 | | |
|---|
| 1017 | | stream.write(buf[0 .. buf_size]); |
|---|
| 1018 | | |
|---|
| 1019 | | pos += buf_size; |
|---|
| 1020 | | if (progress !is null) |
|---|
| 1021 | | progress(pos); |
|---|
| 1022 | | |
|---|
| 1023 | | // Give it more time as long as data is going through. |
|---|
| 1024 | | end_time = Clock.now + this.timeout; |
|---|
| 1025 | | } |
|---|
| 1026 | | |
|---|
| 1027 | | // Did all the data get received? |
|---|
| 1028 | | if (!completed) |
|---|
| 1029 | | throw new FtpException("CLIENT: Timeout when reading data", "420"); |
|---|
| 1030 | | } |
|---|
| 1031 | | |
|---|
| 1032 | | protected void sendStream(SocketConduit data, InputStream stream, FtpProgress progress = null) |
|---|
| 1033 | | in |
|---|
| 1034 | | { |
|---|
| 1035 | | assert (data !is null); |
|---|
| 1036 | | assert (stream !is null); |
|---|
| 1037 | | } |
|---|
| 1038 | | body |
|---|
| 1039 | | { |
|---|
| 1040 | | // Set up a SocketSet so we can use select() - it's pretty efficient. |
|---|
| 1041 | | SocketSet set = new SocketSet(); |
|---|
| 1042 | | scope (exit) |
|---|
| 1043 | | delete set; |
|---|
| 1044 | | |
|---|
| 1045 | | // At end_time, we bail. |
|---|
| 1046 | | Time end_time = Clock.now + this.timeout; |
|---|
| 1047 | | |
|---|
| 1048 | | // This is the buffer the stream data is stored in. |
|---|
| 1049 | | ubyte[8 * 1024] buf; |
|---|
| 1050 | | size_t buf_size = 0, buf_pos = 0; |
|---|
| 1051 | | int delta = 0; |
|---|
| 1052 | | |
|---|
| 1053 | | size_t pos = 0; |
|---|
| 1054 | | bool completed = false; |
|---|
| 1055 | | while (!completed && Clock.now < end_time) |
|---|
| 1056 | | { |
|---|
| 1057 | | set.reset(); |
|---|
| 1058 | | set.add(data.socket); |
|---|
| 1059 | | |
|---|
| 1060 | | // Can we write yet, can we write yet? |
|---|
| 1061 | | int code = Socket.select(null, set, null, this.timeout); |
|---|
| 1062 | | if (code == -1 || code == 0) |
|---|
| 1063 | | break; |
|---|
| 1064 | | |
|---|
| 1065 | | if (buf_size - buf_pos <= 0) |
|---|
| 1066 | | { |
|---|
| 1067 | | if ((buf_size = stream.read(buf)) is stream.Eof) |
|---|
| 1068 | | buf_size = 0, completed = true; |
|---|
| 1069 | | buf_pos = 0; |
|---|
| 1070 | | } |
|---|
| 1071 | | |
|---|
| 1072 | | // Send the chunk (or as much of it as possible!) |
|---|
| 1073 | | delta = data.socket.send(buf[buf_pos .. buf_size]); |
|---|
| 1074 | | if (delta == data.socket.ERROR) |
|---|
| 1075 | | break; |
|---|
| 1076 | | |
|---|
| 1077 | | buf_pos += delta; |
|---|
| 1078 | | |
|---|
| 1079 | | pos += delta; |
|---|
| 1080 | | if (progress !is null) |
|---|
| 1081 | | progress(pos); |
|---|
| 1082 | | |
|---|
| 1083 | | // Give it more time as long as data is going through. |
|---|
| 1084 | | if (delta != 0) |
|---|
| 1085 | | end_time = Clock.now + this.timeout; |
|---|
| 1086 | | } |
|---|
| 1087 | | |
|---|
| 1088 | | // Did all the data get sent? |
|---|
| 1089 | | if (!completed) |
|---|
| 1090 | | throw new FtpException("CLIENT: Timeout when sending data", "420"); |
|---|
| 1091 | | } |
|---|
| 1092 | | |
|---|
| 1093 | | protected FtpFileInfo[] sendListCommand(char[] path) |
|---|
| 1094 | | { |
|---|
| 1095 | | FtpFileInfo[] dir; |
|---|
| 1096 | | SocketConduit data = null; |
|---|
| 1097 | | |
|---|
| 1098 | | if (path.length > 0) |
|---|
| 1099 | | data = this.processDataCommand("LIST", path); |
|---|
| 1100 | | else |
|---|
| 1101 | | data = this.processDataCommand("LIST"); |
|---|
| 1102 | | |
|---|
| 1103 | | // Read in the stupid non-standardized response. |
|---|
| 1104 | | auto listing = new GrowBuffer; |
|---|
| 1105 | | this.readStream(data, listing); |
|---|
| 1106 | | this.finishDataCommand(data); |
|---|
| 1107 | | |
|---|
| 1108 | | // Split out the lines. Most of the time, it's one-to-one. |
|---|
| 1109 | | char[][] lines = Text.splitLines (cast(char[]) listing.slice()); |
|---|
| 1110 | | scope (exit) |
|---|
| 1111 | | delete lines; |
|---|
| 1112 | | |
|---|
| 1113 | | foreach (char[] line; lines) |
|---|
| 1114 | | { |
|---|
| 1115 | | // If there are no spaces, or if there's only one... skip the line. |
|---|
| 1116 | | // This is probably like a "total 8" line. |
|---|
| 1117 | | if (Text.locate(line, ' ') == Text.locatePrior(line, ' ')) |
|---|
| 1118 | | continue; |
|---|
| 1119 | | |
|---|
| 1120 | | // Now parse the line, or try to. |
|---|
| 1121 | | FtpFileInfo info = this.parseListLine(line); |
|---|
| 1122 | | if (info.name.length > 0) |
|---|
| 1123 | | dir ~= info; |
|---|
| 1124 | | } |
|---|
| 1125 | | |
|---|
| 1126 | | return dir; |
|---|
| 1127 | | } |
|---|
| 1128 | | |
|---|
| 1129 | | protected FtpFileInfo parseListLine(char[] line) |
|---|
| 1130 | | { |
|---|
| 1131 | | FtpFileInfo info; |
|---|
| 1132 | | size_t pos = 0; |
|---|
| 1133 | | |
|---|
| 1134 | | // Convenience function to parse a word from the line. |
|---|
| 1135 | | char[] parse_word() |
|---|
| 1136 | | { |
|---|
| 1137 | | size_t start = 0, end = 0; |
|---|
| 1138 | | |
|---|
| 1139 | | // Skip whitespace before. |
|---|
| 1140 | | while (pos < line.length && line[pos] == ' ') |
|---|
| 1141 | | pos++; |
|---|
| 1142 | | |
|---|
| 1143 | | start = pos; |
|---|
| 1144 | | while (pos < line.length && line[pos] != ' ') |
|---|
| 1145 | | pos++; |
|---|
| 1146 | | end = pos; |
|---|
| 1147 | | |
|---|
| 1148 | | // Skip whitespace after. |
|---|
| 1149 | | while (pos < line.length && line[pos] == ' ') |
|---|
| 1150 | | pos++; |
|---|
| 1151 | | |
|---|
| 1152 | | return line[start .. end]; |
|---|
| 1153 | | } |
|---|
| 1154 | | |
|---|
| 1155 | | // We have to sniff this... :/. |
|---|
| 1156 | | switch (! Text.contains ("0123456789", line[0])) |
|---|
| 1157 | | { |
|---|
| 1158 | | // Not a number; this is UNIX format. |
|---|
| 1159 | | case true: |
|---|
| 1160 | | // The line must be at least 20 characters long. |
|---|
| 1161 | | if (line.length < 20) |
|---|
| 1162 | | return info; |
|---|
| 1163 | | |
|---|
| 1164 | | // The first character tells us what it is. |
|---|
| 1165 | | if (line[0] == 'd') |
|---|
| 1166 | | info.type = FtpFileType.dir; |
|---|
| 1167 | | else if (line[0] == '-') |
|---|
| 1168 | | info.type = FtpFileType.file; |
|---|
| 1169 | | else |
|---|
| 1170 | | info.type = FtpFileType.unknown; |
|---|
| 1171 | | |
|---|
| 1172 | | // Parse out the mode... rwxrwxrwx = 777. |
|---|
| 1173 | | char[] unix_mode = "0000".dup; |
|---|
| 1174 | | void read_mode(int digit) |
|---|
| 1175 | | { |
|---|
| 1176 | | for (pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) |
|---|
| 1177 | | { |
|---|
| 1178 | | if (line[pos] == 'r') |
|---|
| 1179 | | unix_mode[digit + 1] |= 4; |
|---|
| 1180 | | else if (line[pos] == 'w') |
|---|
| 1181 | | unix_mode[digit + 1] |= 2; |
|---|
| 1182 | | else if (line[pos] == 'x') |
|---|
| 1183 | | unix_mode[digit + 1] |= 1; |
|---|
| 1184 | | } |
|---|
| 1185 | | } |
|---|
| 1186 | | |
|---|
| 1187 | | // This makes it easier, huh? |
|---|
| 1188 | | read_mode(0); |
|---|
| 1189 | | read_mode(1); |
|---|
| 1190 | | read_mode(2); |
|---|
| 1191 | | |
|---|
| 1192 | | info.facts["UNIX.mode"] = unix_mode; |
|---|
| 1193 | | |
|---|
| 1194 | | // Links, owner, group. These are hard to translate to MLST facts. |
|---|
| 1195 | | parse_word(); |
|---|
| 1196 | | parse_word(); |
|---|
| 1197 | | parse_word(); |
|---|
| 1198 | | |
|---|
| 1199 | | // Size in bytes, this one is good. |
|---|
| 1200 | | info.size = toLong(parse_word()); |
|---|
| 1201 | | |
|---|
| 1202 | | // Make sure we still have enough space. |
|---|
| 1203 | | if (pos + 13 >= line.length) |
|---|
| 1204 | | return info; |
|---|
| 1205 | | |
|---|
| 1206 | | // Not parsing date for now. It's too weird (last 12 months, etc.) |
|---|
| 1207 | | pos += 13; |
|---|
| 1208 | | |
|---|
| 1209 | | info.name = line[pos .. line.length]; |
|---|
| 1210 | | break; |
|---|
| 1211 | | |
|---|
| 1212 | | // A number; this is DOS format. |
|---|
| 1213 | | case false: |
|---|
| 1214 | | // We need some data here, to parse. |
|---|
| 1215 | | if (line.length < 18) |
|---|
| 1216 | | return info; |
|---|
| 1217 | | |
|---|
| 1218 | | // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P |
|---|
| 1219 | | auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); |
|---|
| 1220 | | if ( r.test(line) ) |
|---|
| 1221 | | return info; |
|---|
| 1222 | | |
|---|
| 1223 | | if (Timestamp.dostime (r.match(0), info.modify) is 0) |
|---|
| 1224 | | info.modify = Time.max; |
|---|
| 1225 | | |
|---|
| 1226 | | pos = r.match(0).length; |
|---|
| 1227 | | delete r; |
|---|
| 1228 | | |
|---|
| 1229 | | // This will either be <DIR>, or a number. |
|---|
| 1230 | | char[] dir_or_size = parse_word(); |
|---|
| 1231 | | |
|---|
| 1232 | | if (dir_or_size.length < 0) |
|---|
| 1233 | | return info; |
|---|
| 1234 | | else if (dir_or_size[0] == '<') |
|---|
| 1235 | | info.type = FtpFileType.dir; |
|---|
| 1236 | | else |
|---|
| 1237 | | info.size = toLong(dir_or_size); |
|---|
| 1238 | | |
|---|
| 1239 | | info.name = line[pos .. line.length]; |
|---|
| 1240 | | break; |
|---|
| 1241 | | |
|---|
| 1242 | | // Something else, not supported. |
|---|
| 1243 | | default: |
|---|
| 1244 | | throw new FtpException("CLIENT: Unsupported LIST format", "501"); |
|---|
| 1245 | | } |
|---|
| 1246 | | |
|---|
| 1247 | | // Try to fix the type? |
|---|
| 1248 | | if (info.name == ".") |
|---|
| 1249 | | info.type = FtpFileType.cdir; |
|---|
| 1250 | | else if (info.name == "..") |
|---|
| 1251 | | info.type = FtpFileType.pdir; |
|---|
| 1252 | | |
|---|
| 1253 | | return info; |
|---|
| 1254 | | } |
|---|
| 1255 | | |
|---|
| 1256 | | protected FtpFileInfo parseMlstLine(char[] line) |
|---|
| 1257 | | { |
|---|
| 1258 | | FtpFileInfo info; |
|---|
| 1259 | | |
|---|
| 1260 | | // After this loop, filename_pos will be location of space + 1. |
|---|
| 1261 | | size_t filename_pos = 0; |
|---|
| 1262 | | while (filename_pos < line.length && line[filename_pos++] != ' ') |
|---|
| 1263 | | continue; |
|---|
| 1264 | | |
|---|
| 1265 | | if (filename_pos == line.length) |
|---|
| 1266 | | throw new FtpException("CLIENT: Bad syntax in MLSx response", "501"); |
|---|
| 1267 | | /*{ |
|---|
| 1268 | | info.name = ""; |
|---|
| 1269 | | return info; |
|---|
| 1270 | | }*/ |
|---|
| 1271 | | |
|---|
| 1272 | | info.name = line[filename_pos .. line.length]; |
|---|
| 1273 | | |
|---|
| 1274 | | // Everything else is frosting on top. |
|---|
| 1275 | | if (filename_pos > 1) |
|---|
| 1276 | | { |
|---|
| 1277 | | char[][] temp_facts = Text.delimit(line[0 .. filename_pos - 1], ";"); |
|---|
| 1278 | | |
|---|
| 1279 | | // Go through each fact and parse them into the array. |
|---|
| 1280 | | foreach (char[] fact; temp_facts) |
|---|
| 1281 | | { |
|---|
| 1282 | | int pos = Text.locate(fact, '='); |
|---|
| 1283 | | if (pos == fact.length) |
|---|
| 1284 | | continue; |
|---|
| 1285 | | |
|---|
| 1286 | | info.facts[Ascii.toLower(fact[0 .. pos])] = fact[pos + 1 .. fact.length]; |
|---|
| 1287 | | } |
|---|
| 1288 | | |
|---|
| 1289 | | // Do we have a type? |
|---|
| 1290 | | if ("type" in info.facts) |
|---|
| 1291 | | { |
|---|
| 1292 | | // Some reflection might be nice here. |
|---|
| 1293 | | switch (Ascii.toLower(info.facts["type"])) |
|---|
| 1294 | | { |
|---|
| 1295 | | case "file": |
|---|
| 1296 | | info.type = FtpFileType.file; |
|---|
| 1297 | | break; |
|---|
| 1298 | | |
|---|
| 1299 | | case "cdir": |
|---|
| 1300 | | info.type = FtpFileType.cdir; |
|---|
| 1301 | | break; |
|---|
| 1302 | | |
|---|
| 1303 | | case "pdir": |
|---|
| 1304 | | info.type = FtpFileType.pdir; |
|---|
| 1305 | | break; |
|---|
| 1306 | | |
|---|
| 1307 | | case "dir": |
|---|
| 1308 | | info.type = FtpFileType.dir; |
|---|
| 1309 | | break; |
|---|
| 1310 | | |
|---|
| 1311 | | default: |
|---|
| 1312 | | info.type = FtpFileType.other; |
|---|
| 1313 | | } |
|---|
| 1314 | | } |
|---|
| 1315 | | |
|---|
| 1316 | | // Size, mime, etc... |
|---|
| 1317 | | if ("size" in info.facts) |
|---|
| 1318 | | info.size = toLong(info.facts["size"]); |
|---|
| 1319 | | if ("media-type" in info.facts) |
|---|
| 1320 | | info.mime = info.facts["media-type"]; |
|---|
| 1321 | | |
|---|
| 1322 | | // And the two dates. |
|---|
| 1323 | | if ("modify" in info.facts) |
|---|
| 1324 | | info.modify = this.parseTimeval(info.facts["modify"]); |
|---|
| 1325 | | if ("create" in info.facts) |
|---|
| 1326 | | info.create = this.parseTimeval(info.facts["create"]); |
|---|
| 1327 | | } |
|---|
| 1328 | | |
|---|
| 1329 | | return info; |
|---|
| 1330 | | } |
|---|
| | 881 | public void finishDataCommand(SocketConduit data) { |
|---|
| | 882 | // Close the socket. This tells the server we're done (EOF.) |
|---|
| | 883 | data.close(); |
|---|
| | 884 | data.detach(); |
|---|
| | 885 | |
|---|
| | 886 | // We shouldn't get a 250 in STREAM mode. |
|---|
| | 887 | this.readResponse("226"); |
|---|
| | 888 | } |
|---|
| | 889 | |
|---|
| | 890 | public SocketConduit processDataCommand(char[] command, char[][] parameters...) { |
|---|
| | 891 | // Create a connection. |
|---|
| | 892 | SocketConduit data = this.getDataSocket(); |
|---|
| | 893 | scope(failure) { |
|---|
| | 894 | // Close the socket, whether we were listening or not. |
|---|
| | 895 | data.close(); |
|---|
| | 896 | } |
|---|
| | 897 | |
|---|
| | 898 | // Tell the server about it. |
|---|
| | 899 | this.sendCommand(command, parameters); |
|---|
| | 900 | |
|---|
| | 901 | // We should always get a 150/125 response. |
|---|
| | 902 | auto response = this.readResponse(); |
|---|
| | 903 | if(response.code != "150" && response.code != "125") |
|---|
| | 904 | exception(response); |
|---|
| | 905 | |
|---|
| | 906 | // We might need to do this for active connections. |
|---|
| | 907 | this.prepareDataSocket(data); |
|---|
| | 908 | |
|---|
| | 909 | return data; |
|---|
| | 910 | } |
|---|
| | 911 | |
|---|
| | 912 | public FtpFileInfo[] ls(char[] path = "") |
|---|
| | 913 | // default to current dir |
|---|
| | 914 | in { |
|---|
| | 915 | assert(path.length == 0 || path[path.length - 1] != '/'); |
|---|
| | 916 | } |
|---|
| | 917 | body { |
|---|
| | 918 | FtpFileInfo[] dir; |
|---|
| | 919 | |
|---|
| | 920 | // We'll try MLSD (which is so much better) first... but it may fail. |
|---|
| | 921 | bool mlsd_success = false; |
|---|
| | 922 | SocketConduit data = null; |
|---|
| | 923 | |
|---|
| | 924 | // Try it if it could/might/maybe is supported. |
|---|
| | 925 | if(this.isSupported("MLST")) { |
|---|
| | 926 | mlsd_success = true; |
|---|
| | 927 | |
|---|
| | 928 | // Since this is a data command, processDataCommand handles |
|---|
| | 929 | // checking the response... just catch its Exception. |
|---|
| | 930 | try { |
|---|
| | 931 | if(path.length > 0) |
|---|
| | 932 | data = this.processDataCommand("MLSD", path); |
|---|
| | 933 | else |
|---|
| | 934 | data = this.processDataCommand("MLSD"); |
|---|
| | 935 | } catch(FtpException) |
|---|
| | 936 | mlsd_success = false; |
|---|
| | 937 | } |
|---|
| | 938 | |
|---|
| | 939 | // If it passed, parse away! |
|---|
| | 940 | if(mlsd_success) { |
|---|
| | 941 | auto listing = new GrowBuffer; |
|---|
| | 942 | this.readStream(data, listing); |
|---|
| | 943 | this.finishDataCommand(data); |
|---|
| | 944 | |
|---|
| | 945 | // Each line is something in that directory. |
|---|
| | 946 | char[][] lines = Text.splitLines(cast(char[]) listing.slice()); |
|---|
| | 947 | scope(exit) |
|---|
| | 948 | delete lines; |
|---|
| | 949 | |
|---|
| | 950 | foreach(char[] line; lines) { |
|---|
| | 951 | // Parse each line exactly like MLST does. |
|---|
| | 952 | try { |
|---|
| | 953 | FtpFileInfo info = this.parseMlstLine(line); |
|---|
| | 954 | if(info.name.length > 0) |
|---|
| | 955 | dir ~= info; |
|---|
| | 956 | } catch(FtpException) { |
|---|
| | 957 | return this.sendListCommand(path); |
|---|
| | 958 | } |
|---|
| | 959 | } |
|---|
| | 960 | |
|---|
| | 961 | return dir; |
|---|
| | 962 | } |
|---|
| | 963 | // Fall back to LIST. |
|---|
| | 964 | else |
|---|
| | 965 | return this.sendListCommand(path); |
|---|
| | 966 | } |
|---|
| | 967 | |
|---|
| | 968 | protected void readStream(SocketConduit data, OutputStream stream, |
|---|
| | 969 | FtpProgress progress = null) |
|---|
| | 970 | in { |
|---|
| | 971 | assert(data !is null); |
|---|
| | 972 | assert(stream !is null); |
|---|
| | 973 | } |
|---|
| | 974 | body { |
|---|
| | 975 | // Set up a SocketSet so we can use select() - it's pretty efficient. |
|---|
| | 976 | SocketSet set = new SocketSet(); |
|---|
| | 977 | scope(exit) |
|---|
| | 978 | delete set; |
|---|
| | 979 | |
|---|
| | 980 | // At end_time, we bail. |
|---|
| | 981 | Time end_time = Clock.now + this.timeout; |
|---|
| | 982 | |
|---|
| | 983 | // This is the buffer the stream data is stored in. |
|---|
| | 984 | ubyte[8 * 1024] buf; |
|---|
| | 985 | int buf_size = 0; |
|---|
| | 986 | |
|---|
| | 987 | bool completed = false; |
|---|
| | 988 | size_t pos; |
|---|
| | 989 | while(Clock.now < end_time) { |
|---|
| | 990 | set.reset(); |
|---|
| | 991 | set.add(data.socket); |
|---|
| | 992 | |
|---|
| | 993 | // Can we read yet, can we read yet? |
|---|
| | 994 | int code = Socket.select(set, null, null, this.timeout); |
|---|
| | 995 | if(code == -1 || code == 0) |
|---|
| | 996 | break; |
|---|
| | 997 | |
|---|
| | 998 | buf_size = data.socket.receive(buf); |
|---|
| | 999 | if(buf_size == data.socket.ERROR) |
|---|
| | 1000 | break; |
|---|
| | 1001 | |
|---|
| | 1002 | if(buf_size == 0) { |
|---|
| | 1003 | completed = true; |
|---|
| | 1004 | break; |
|---|
| | 1005 | } |
|---|
| | 1006 | |
|---|
| | 1007 | stream.write(buf[0 .. buf_size]); |
|---|
| | 1008 | |
|---|
| | 1009 | pos += buf_size; |
|---|
| | 1010 | if(progress !is null) |
|---|
| | 1011 | progress(pos); |
|---|
| | 1012 | |
|---|
| | 1013 | // Give it more time as long as data is going through. |
|---|
| | 1014 | end_time = Clock.now + this.timeout; |
|---|
| | 1015 | } |
|---|
| | 1016 | |
|---|
| | 1017 | // Did all the data get received? |
|---|
| | 1018 | if(!completed) |
|---|
| | 1019 | throw new FtpException("CLIENT: Timeout when reading data", "420"); |
|---|
| | 1020 | } |
|---|
| | 1021 | |
|---|
| | 1022 | protected void sendStream(SocketConduit data, InputStream stream, |
|---|
| | 1023 | FtpProgress progress = null) |
|---|
| | 1024 | in { |
|---|
| | 1025 | assert(data !is null); |
|---|
| | 1026 | assert(stream !is null); |
|---|
| | 1027 | } |
|---|
| | 1028 | body { |
|---|
| | 1029 | // Set up a SocketSet so we can use select() - it's pretty efficient. |
|---|
| | 1030 | SocketSet set = new SocketSet(); |
|---|
| | 1031 | scope(exit) |
|---|
| | 1032 | delete set; |
|---|
| | 1033 | |
|---|
| | 1034 | // At end_time, we bail. |
|---|
| | 1035 | Time end_time = Clock.now + this.timeout; |
|---|
| | 1036 | |
|---|
| | 1037 | // This is the buffer the stream data is stored in. |
|---|
| | 1038 | ubyte[8 * 1024] buf; |
|---|
| | 1039 | size_t buf_size = 0, buf_pos = 0; |
|---|
| | 1040 | int delta = 0; |
|---|
| | 1041 | |
|---|
| | 1042 | size_t pos = 0; |
|---|
| | 1043 | bool completed = false; |
|---|
| | 1044 | while(!completed && Clock.now < end_time) { |
|---|
| | 1045 | set.reset(); |
|---|
| | 1046 | set.add(data.socket); |
|---|
| | 1047 | |
|---|
| | 1048 | // Can we write yet, can we write yet? |
|---|
| | 1049 | int code = Socket.select(null, set, null, this.timeout); |
|---|
| | 1050 | if(code == -1 || code == 0) |
|---|
| | 1051 | break; |
|---|
| | 1052 | |
|---|
| | 1053 | if(buf_size - buf_pos <= 0) { |
|---|
| | 1054 | if((buf_size = stream.read(buf)) is stream.Eof) |
|---|
| | 1055 | buf_size = 0 , completed = true; |
|---|
| | 1056 | buf_pos = 0; |
|---|
| | 1057 | } |
|---|
| | 1058 | |
|---|
| | 1059 | // Send the chunk (or as much of it as possible!) |
|---|
| | 1060 | delta = data.socket.send(buf[buf_pos .. buf_size]); |
|---|
| | 1061 | if(delta == data.socket.ERROR) |
|---|
| | 1062 | break; |
|---|
| | 1063 | |
|---|
| | 1064 | buf_pos += delta; |
|---|
| | 1065 | |
|---|
| | 1066 | pos += delta; |
|---|
| | 1067 | if(progress !is null) |
|---|
| | 1068 | progress(pos); |
|---|
| | 1069 | |
|---|
| | 1070 | // Give it more time as long as data is going through. |
|---|
| | 1071 | if(delta != 0) |
|---|
| | 1072 | end_time = Clock.now + this.timeout; |
|---|
| | 1073 | } |
|---|
| | 1074 | |
|---|
| | 1075 | // Did all the data get sent? |
|---|
| | 1076 | if(!completed) |
|---|
| | 1077 | throw new FtpException("CLIENT: Timeout when sending data", "420"); |
|---|
| | 1078 | } |
|---|
| | 1079 | |
|---|
| | 1080 | protected FtpFileInfo[] sendListCommand(char[] path) { |
|---|
| | 1081 | FtpFileInfo[] dir; |
|---|
| | 1082 | SocketConduit data = null; |
|---|
| | 1083 | |
|---|
| | 1084 | if(path.length > 0) |
|---|
| | 1085 | data = this.processDataCommand("LIST", path); |
|---|
| | 1086 | else |
|---|
| | 1087 | data = this.processDataCommand("LIST"); |
|---|
| | 1088 | |
|---|
| | 1089 | // Read in the stupid non-standardized response. |
|---|
| | 1090 | auto listing = new GrowBuffer; |
|---|
| | 1091 | this.readStream(data, listing); |
|---|
| | 1092 | this.finishDataCommand(data); |
|---|
| | 1093 | |
|---|
| | 1094 | // Split out the lines. Most of the time, it's one-to-one. |
|---|
| | 1095 | char[][] lines = Text.splitLines(cast(char[]) listing.slice()); |
|---|
| | 1096 | scope(exit) |
|---|
| | 1097 | delete lines; |
|---|
| | 1098 | |
|---|
| | 1099 | foreach(char[] line; lines) { |
|---|
| | 1100 | // If there are no spaces, or if there's only one... skip the line. |
|---|
| | 1101 | // This is probably like a "total 8" line. |
|---|
| | 1102 | if(Text.locate(line, ' ') == Text.locatePrior(line, ' ')) |
|---|
| | 1103 | continue; |
|---|
| | 1104 | |
|---|
| | 1105 | // Now parse the line, or try to. |
|---|
| | 1106 | FtpFileInfo info = this.parseListLine(line); |
|---|
| | 1107 | if(info.name.length > 0) |
|---|
| | 1108 | dir ~= info; |
|---|
| | 1109 | } |
|---|
| | 1110 | |
|---|
| | 1111 | return dir; |
|---|
| | 1112 | } |
|---|
| | 1113 | |
|---|
| | 1114 | protected FtpFileInfo parseListLine(char[] line) { |
|---|
| | 1115 | FtpFileInfo info; |
|---|
| | 1116 | size_t pos = 0; |
|---|
| | 1117 | |
|---|
| | 1118 | // Convenience function to parse a word from the line. |
|---|
| | 1119 | char[] parse_word() { |
|---|
| | 1120 | size_t start = 0, end = 0; |
|---|
| | 1121 | |
|---|
| | 1122 | // Skip whitespace before. |
|---|
| | 1123 | while(pos < line.length && line[pos] == ' ') |
|---|
| | 1124 | pos++; |
|---|
| | 1125 | |
|---|
| | 1126 | start = pos; |
|---|
| | 1127 | while(pos < line.length && line[pos] != ' ') |
|---|
| | 1128 | pos++; |
|---|
| | 1129 | end = pos; |
|---|
| | 1130 | |
|---|
| | 1131 | // Skip whitespace after. |
|---|
| | 1132 | while(pos < line.length && line[pos] == ' ') |
|---|
| | 1133 | pos++; |
|---|
| | 1134 | |
|---|
| | 1135 | return line[start .. end]; |
|---|
| | 1136 | } |
|---|
| | 1137 | |
|---|
| | 1138 | // We have to sniff this... :/. |
|---|
| | 1139 | switch(!Text.contains("0123456789", line[0])) { |
|---|
| | 1140 | // Not a number; this is UNIX format. |
|---|
| | 1141 | case true: |
|---|
| | 1142 | // The line must be at least 20 characters long. |
|---|
| | 1143 | if(line.length < 20) |
|---|
| | 1144 | return info; |
|---|
| | 1145 | |
|---|
| | 1146 | // The first character tells us what it is. |
|---|
| | 1147 | if(line[0] == 'd') |
|---|
| | 1148 | info.type = FtpFileType.dir; |
|---|
| | 1149 | else if(line[0] == '-') |
|---|
| | 1150 | info.type = FtpFileType.file; |
|---|
| | 1151 | else |
|---|
| | 1152 | info.type = FtpFileType.unknown; |
|---|
| | 1153 | |
|---|
| | 1154 | // Parse out the mode... rwxrwxrwx = 777. |
|---|
| | 1155 | char[] unix_mode = "0000".dup; |
|---|
| | 1156 | void read_mode(int digit) { |
|---|
| | 1157 | for(pos = 1 + digit * 3; pos <= 3 + digit * 3; pos++) { |
|---|
| | 1158 | if(line[pos] == 'r') |
|---|
| | 1159 | unix_mode[digit + 1] |= 4; |
|---|
| | 1160 | else if(line[pos] == 'w') |
|---|
| | 1161 | unix_mode[digit + 1] |= 2; |
|---|
| | 1162 | else if(line[pos] == 'x') |
|---|
| | 1163 | unix_mode[digit + 1] |= 1; |
|---|
| | 1164 | } |
|---|
| | 1165 | } |
|---|
| | 1166 | |
|---|
| | 1167 | // This makes it easier, huh? |
|---|
| | 1168 | read_mode(0); |
|---|
| | 1169 | read_mode(1); |
|---|
| | 1170 | read_mode(2); |
|---|
| | 1171 | |
|---|
| | 1172 | info.facts["UNIX.mode"] = unix_mode; |
|---|
| | 1173 | |
|---|
| | 1174 | // Links, owner, group. These are hard to translate to MLST facts. |
|---|
| | 1175 | parse_word(); |
|---|
| | 1176 | parse_word(); |
|---|
| | 1177 | parse_word(); |
|---|
| | 1178 | |
|---|
| | 1179 | // Size in bytes, this one is good. |
|---|
| | 1180 | info.size = toLong(parse_word()); |
|---|
| | 1181 | |
|---|
| | 1182 | // Make sure we still have enough space. |
|---|
| | 1183 | if(pos + 13 >= line.length) |
|---|
| | 1184 | return info; |
|---|
| | 1185 | |
|---|
| | 1186 | // Not parsing date for now. It's too weird (last 12 months, etc.) |
|---|
| | 1187 | pos += 13; |
|---|
| | 1188 | |
|---|
| | 1189 | info.name = line[pos .. line.length]; |
|---|
| | 1190 | break; |
|---|
| | 1191 | |
|---|
| | 1192 | // A number; this is DOS format. |
|---|
| | 1193 | case false: |
|---|
| | 1194 | // We need some data here, to parse. |
|---|
| | 1195 | if(line.length < 18) |
|---|
| | 1196 | return info; |
|---|
| | 1197 | |
|---|
| | 1198 | // The order is 1 MM, 2 DD, 3 YY, 4 HH, 5 MM, 6 P |
|---|
| | 1199 | auto r = Regex(`(\d\d)-(\d\d)-(\d\d)\s+(\d\d):(\d\d)(A|P)M`); |
|---|
| | 1200 | if(r.test(line)) |
|---|
| | 1201 | return info; |
|---|
| | 1202 | |
|---|
| | 1203 | if(Timestamp.dostime(r.match(0), info.modify) is 0) |
|---|
| | 1204 | info.modify = Time.max; |
|---|
| | 1205 | |
|---|
| | 1206 | pos = r.match(0).length; |
|---|
| | 1207 | delete r; |
|---|
| | 1208 | |
|---|
| | 1209 | // This will either be <DIR>, or a number. |
|---|
| | 1210 | char[] dir_or_size = parse_word(); |
|---|
| | 1211 | |
|---|
| | 1212 | if(dir_or_size.length < 0) |
|---|
| | 1213 | return info; |
|---|
| | 1214 | else if(dir_or_size[0] == '<') |
|---|
| | 1215 | info.type = FtpFileType.dir; |
|---|
| | 1216 | else |
|---|
| | 1217 | info.size = toLong(dir_or_size); |
|---|
| | 1218 | |
|---|
| | 1219 | info.name = line[pos .. line.length]; |
|---|
| | 1220 | break; |
|---|
| | 1221 | |
|---|
| | 1222 | // Something else, not supported. |
|---|
| | 1223 | default: |
|---|
| | 1224 | throw new FtpException("CLIENT: Unsupported LIST format", "501"); |
|---|
| | 1225 | } |
|---|
| | 1226 | |
|---|
| | 1227 | // Try to fix the type? |
|---|
| | 1228 | if(info.name == ".") |
|---|
| | 1229 | info.type = FtpFileType.cdir; |
|---|
| | 1230 | else if(info.name == "..") |
|---|
| | 1231 | info.type = FtpFileType.pdir; |
|---|
| | 1232 | |
|---|
| | 1233 | return info; |
|---|
| | 1234 | } |
|---|
| | 1235 | |
|---|
| | 1236 | protected FtpFileInfo parseMlstLine(char[] line) { |
|---|
| | 1237 | FtpFileInfo info; |
|---|
| | 1238 | |
|---|
| | 1239 | // After this loop, filename_pos will be location of space + 1. |
|---|
| | 1240 | size_t filename_pos = 0; |
|---|
| | 1241 | while(filename_pos < line.length && line[filename_pos++] != ' ') |
|---|
| | 1242 | continue; |
|---|
| | 1243 |
|---|