Skip to content

Commit f7c182a

Browse files
committed
Release v0.68.0
1 parent a34f870 commit f7c182a

17 files changed

Lines changed: 985 additions & 30 deletions

api/versions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
continue;
4747
}
4848

49-
// 验证版本号格式(例如:0.67.0)
49+
// 验证版本号格式(例如:0.68.0)
5050
if (!preg_match('/^[0-9]+\.[0-9]+\.[0-9]+$/', $item)) {
5151
continue;
5252
}

app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ android {
3030
minSdkVersion 19
3131
targetSdkVersion 31
3232
versionCode 1
33-
versionName "0.67.0"
33+
versionName "0.68.0"
3434
multiDexEnabled true
3535

3636
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -96,7 +96,7 @@ dependencies {
9696
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
9797

9898
// 不再编译时依赖 frp.aar,改为运行时动态加载
99-
// implementation(name: 'frp', ext: 'aar', group: '', version: '0.67.0')
99+
// implementation(name: 'frp', ext: 'aar', group: '', version: '0.68.0')
100100

101101
implementation 'androidx.recyclerview:recyclerview:1.1.0'
102102
implementation 'com.jakewharton:butterknife:10.2.1'
53.9 MB
Binary file not shown.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package max.plus.frp;
2+
3+
import android.content.Context;
4+
5+
import java.io.File;
6+
import java.io.FileInputStream;
7+
import java.io.FileOutputStream;
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.regex.Matcher;
11+
import java.util.regex.Pattern;
12+
13+
public class ConfigFileStore {
14+
private static final Pattern TOML_PATH_QUOTED = Pattern.compile("(?m)^(\\s*path\\s*=\\s*)\"\\./([^\"]+)\"(\\s*(?:#.*)?)$");
15+
private static final Pattern TOML_PATH_RAW = Pattern.compile("(?m)^(\\s*path\\s*=\\s*)\\./([^\\s#;]+)(\\s*(?:#.*)?)$");
16+
private static final Pattern JSON_YAML_PATH = Pattern.compile("(?m)^(\\s*\"?path\"?\\s*:\\s*)\"\\./([^\"]+)\"(\\s*,?\\s*)$");
17+
18+
private static final String DIR_FRPC = "frpc";
19+
private static final String DIR_FRPS = "frps";
20+
21+
private ConfigFileStore() {
22+
}
23+
24+
public static File getConfigFile(Context context, String type, String uid, String name, String format) {
25+
String safeType = "frps".equalsIgnoreCase(type) ? DIR_FRPS : DIR_FRPC;
26+
String safeFormat = ConfigFormatUtils.normalizeFormat(format);
27+
File typeRoot = new File(context.getFilesDir(), safeType);
28+
if (!typeRoot.exists()) {
29+
typeRoot.mkdirs();
30+
}
31+
32+
String safeUid = sanitizeFileName(uid);
33+
if (safeUid.isEmpty()) {
34+
safeUid = "unknown";
35+
}
36+
File uidDir = new File(typeRoot, safeUid);
37+
if (!uidDir.exists()) {
38+
uidDir.mkdirs();
39+
}
40+
41+
String baseName = sanitizeFileName(name);
42+
if (baseName.isEmpty()) {
43+
baseName = DIR_FRPS.equals(safeType) ? "frps" : "frpc";
44+
}
45+
// DB 中 name 不带格式;落盘文件名统一拼接 format 后缀
46+
return new File(uidDir, baseName + "." + safeFormat);
47+
}
48+
49+
public static File writeConfigAtomic(Context context, String type, String uid, String name, String format, String content) throws IOException {
50+
File target = getConfigFile(context, type, uid, name, format);
51+
File parent = target.getParentFile();
52+
if (parent != null && !parent.exists()) {
53+
parent.mkdirs();
54+
}
55+
String normalized = normalizeStorePath(content == null ? "" : content, parent);
56+
// 内容一致时直接复用,避免不必要的写盘与 FileObserver 触发
57+
if (target.exists()) {
58+
String oldContent = readUtf8(target);
59+
if (oldContent.equals(normalized)) {
60+
return target;
61+
}
62+
}
63+
writeExistingFileAtomic(target, normalized);
64+
return target;
65+
}
66+
67+
public static String readUtf8(File file) throws IOException {
68+
byte[] buf = new byte[(int) file.length()];
69+
try (FileInputStream fis = new FileInputStream(file)) {
70+
int read = fis.read(buf);
71+
if (read <= 0) {
72+
return "";
73+
}
74+
return new String(buf, 0, read, StandardCharsets.UTF_8);
75+
}
76+
}
77+
78+
public static void writeExistingFileAtomic(File target, String content) throws IOException {
79+
File parent = target.getParentFile();
80+
if (parent != null && !parent.exists()) {
81+
parent.mkdirs();
82+
}
83+
File temp = new File(parent, target.getName() + ".tmp");
84+
try (FileOutputStream fos = new FileOutputStream(temp, false)) {
85+
byte[] data = (content == null ? "" : content).getBytes(StandardCharsets.UTF_8);
86+
fos.write(data);
87+
fos.flush();
88+
}
89+
if (target.exists() && !target.delete()) {
90+
throw new IOException("Failed to replace old config file: " + target.getAbsolutePath());
91+
}
92+
if (!temp.renameTo(target)) {
93+
throw new IOException("Failed to rename temp config file: " + temp.getAbsolutePath());
94+
}
95+
}
96+
97+
public static String normalizeStorePath(String content, File configDir) {
98+
if (content == null || content.isEmpty() || configDir == null) {
99+
return content == null ? "" : content;
100+
}
101+
String normalized = content;
102+
normalized = replaceWithAbsolute(TOML_PATH_QUOTED, normalized, configDir);
103+
normalized = replaceWithAbsolute(TOML_PATH_RAW, normalized, configDir);
104+
normalized = replaceWithAbsolute(JSON_YAML_PATH, normalized, configDir);
105+
return normalized;
106+
}
107+
108+
private static String replaceWithAbsolute(Pattern pattern, String source, File configDir) {
109+
Matcher matcher = pattern.matcher(source);
110+
StringBuffer sb = new StringBuffer();
111+
while (matcher.find()) {
112+
String prefix = matcher.group(1);
113+
String relative = matcher.group(2);
114+
String suffix = matcher.group(3);
115+
File abs = new File(configDir, relative);
116+
String absPath = abs.getAbsolutePath().replace("\\", "/");
117+
String replacement = prefix + "\"" + absPath + "\"" + suffix;
118+
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
119+
}
120+
matcher.appendTail(sb);
121+
return sb.toString();
122+
}
123+
124+
private static String sanitizeFileName(String name) {
125+
if (name == null) {
126+
return "";
127+
}
128+
String n = name.trim();
129+
n = n.replaceAll("[\\\\/:*?\"<>|]", "_");
130+
n = n.replaceAll("\\s+", "_");
131+
return n;
132+
}
133+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package max.plus.frp;
2+
3+
public class ConfigFormatUtils {
4+
5+
private ConfigFormatUtils() {
6+
}
7+
8+
public static String normalizeFormat(String format) {
9+
if (format == null) {
10+
return "toml";
11+
}
12+
String f = format.trim().toLowerCase();
13+
if (f.isEmpty()) {
14+
return "toml";
15+
}
16+
if ("yml".equals(f)) {
17+
return "yaml";
18+
}
19+
if ("ini".equals(f) || "toml".equals(f) || "yaml".equals(f) || "json".equals(f)) {
20+
return f;
21+
}
22+
return "toml";
23+
}
24+
25+
public static String inferFormatFromContent(String content) {
26+
if (content == null || content.trim().isEmpty()) {
27+
return "toml";
28+
}
29+
30+
String trimmed = content.trim();
31+
// JSON object or array
32+
if (trimmed.startsWith("{") || startsWithJsonArray(trimmed)) {
33+
return "json";
34+
}
35+
36+
// YAML: key: value 风格,且通常不使用 '='
37+
if (trimmed.contains(":") && !trimmed.contains("=") && !trimmed.startsWith("[")) {
38+
String[] lines = trimmed.split("\n");
39+
for (String line : lines) {
40+
if (line.matches("^\\s*[a-zA-Z_][a-zA-Z0-9_\\-]*:.*")) {
41+
return "yaml";
42+
}
43+
}
44+
}
45+
46+
// TOML 常见特征:[[array]]、引号值、驼峰键/点键
47+
if (trimmed.contains("[[")) {
48+
return "toml";
49+
}
50+
if (trimmed.matches("(?s).*^[a-zA-Z0-9_.-]+\\s*=\\s*\".*$.*")) {
51+
return "toml";
52+
}
53+
54+
// INI 常见特征:[section] + key = value(含下划线键)
55+
if (trimmed.contains("[") && trimmed.contains("]") && trimmed.contains("=")) {
56+
return "ini";
57+
}
58+
59+
return "toml";
60+
}
61+
62+
public static String inferFormatFromFileName(String fileName) {
63+
if (fileName == null) {
64+
return "";
65+
}
66+
int idx = fileName.lastIndexOf('.');
67+
if (idx < 0 || idx >= fileName.length() - 1) {
68+
return "";
69+
}
70+
String ext = normalizeFormat(fileName.substring(idx + 1));
71+
if ("ini".equals(ext) || "toml".equals(ext) || "yaml".equals(ext) || "json".equals(ext)) {
72+
return ext;
73+
}
74+
return "";
75+
}
76+
77+
public static String inferFormatAfterFileEdited(String fileName, String content, String currentFormat) {
78+
String byContent = inferFormatFromContent(content);
79+
if (!byContent.isEmpty()) {
80+
return byContent;
81+
}
82+
String byName = inferFormatFromFileName(fileName);
83+
if (!byName.isEmpty()) {
84+
return byName;
85+
}
86+
return normalizeFormat(currentFormat);
87+
}
88+
89+
private static boolean startsWithJsonArray(String trimmed) {
90+
if (!trimmed.startsWith("[")) {
91+
return false;
92+
}
93+
int i = 1;
94+
while (i < trimmed.length() && Character.isWhitespace(trimmed.charAt(i))) {
95+
i++;
96+
}
97+
if (i >= trimmed.length()) {
98+
return false;
99+
}
100+
char c = trimmed.charAt(i);
101+
return c == '{' || c == '\"' || c == '[' || c == '-' || Character.isDigit(c);
102+
}
103+
}

0 commit comments

Comments
 (0)