{"record":{"id":"3a016200c4731464","repo":"termux/termux-app","slug":"malformed-symlink-line","errorCode":null,"errorMessage":"Malformed symlink line: ","messagePattern":"Malformed symlink line: ","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"critical","filePath":"app/src/main/java/com/termux/app/TermuxInstaller.java","lineNumber":169,"sourceCode":"                        return;\n                    }\n\n                    Logger.logInfo(LOG_TAG, \"Extracting bootstrap zip to prefix staging directory \\\"\" + TERMUX_STAGING_PREFIX_DIR_PATH + \"\\\".\");\n\n                    final byte[] buffer = new byte[8096];\n                    final List<Pair<String, String>> symlinks = new ArrayList<>(50);\n\n                    final byte[] zipBytes = loadZipBytes();\n                    try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {\n                        ZipEntry zipEntry;\n                        while ((zipEntry = zipInput.getNextEntry()) != null) {\n                            if (zipEntry.getName().equals(\"SYMLINKS.txt\")) {\n                                BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));\n                                String line;\n                                while ((line = symlinksReader.readLine()) != null) {\n                                    String[] parts = line.split(\"←\");\n                                    if (parts.length != 2)\n                                        throw new RuntimeException(\"Malformed symlink line: \" + line);\n                                    String oldPath = parts[0];\n                                    String newPath = TERMUX_STAGING_PREFIX_DIR_PATH + \"/\" + parts[1];\n                                    symlinks.add(Pair.create(oldPath, newPath));\n\n                                    error = ensureDirectoryExists(new File(newPath).getParentFile());\n                                    if (error != null) {\n                                        showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));\n                                        return;\n                                    }\n                                }\n                            } else {\n                                String zipEntryName = zipEntry.getName();\n                                File targetFile = new File(TERMUX_STAGING_PREFIX_DIR_PATH, zipEntryName);\n                                boolean isDirectory = zipEntry.isDirectory();\n\n                                error = ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());\n                                if (error != null) {\n                                    showBootstrapErrorDialog(activity, whenDone, Error.getErrorMarkdownString(error));","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/termux/termux-app/blob/3df69d1da197dd9bd71a3bafd902dffd720576b4/app/src/main/java/com/termux/app/TermuxInstaller.java#L151-L187","documentation":"Thrown during Termux bootstrap installation while parsing the SYMLINKS.txt entry inside the bootstrap zip. Each line in SYMLINKS.txt must contain exactly one '←' character so that line.split(\"←\") yields exactly two parts (oldPath, newPath). If a line has zero, two, or more '←' separators the split produces != 2 parts and this RuntimeException is raised, aborting the whole prefix setup.","triggerScenarios":"A bootstrap zip ships a SYMLINKS.txt entry where a line is blank, contains no '←' (e.g. uses '->' or ASCII arrow), contains multiple '←', or is a stray comment/header line. Also triggered if the file uses a different delimiter than the code expects or has CRLF corruption that shifts parsing.","commonSituations":"Custom/modified bootstrap zip with a hand-edited SYMLINKS.txt; bootstrap generated by a different toolchain that uses '->' instead of '←'; a corrupted download that garbled delimiter bytes; end-of-file trailing blank line that is not skipped.","solutions":["Inspect the failing line value in the exception message; it is appended verbatim after 'Malformed symlink line: '.","Open the bootstrap zip and examine SYMLINKS.txt; ensure every non-empty line has exactly one '←' separating the link target from the install-relative path.","Regenerate the bootstrap zip with a known-good termux-packages build so the delimiter matches, or strip/ignore blank lines in the parser.","If maintaining a fork, change the parser's expected delimiter consistently with how SYMLINKS.txt is generated."],"exampleFix":"// before\nString[] parts = line.split(\"←\");\nif (parts.length != 2)\n    throw new RuntimeException(\"Malformed symlink line: \" + line);\n\n// after (tolerate blank lines, validate delimiter)\nif (line.isEmpty()) continue;\nString[] parts = line.split(\"←\");\nif (parts.length != 2)\n    throw new RuntimeException(\"Malformed symlink line (expected one '←'): \" + line);","handlingStrategy":"validation","validationCode":"// Validate each SYMLINKS.txt line before splitting\nif (line.isEmpty() || line.startsWith(\"#\")) continue;\nlong arrowCount = line.chars().filter(c -> c == '\\u2190').count();\nif (arrowCount != 1) {\n    throw new IOException(\"Expected exactly one '\\u2190' in line: \" + line);\n}\nString[] parts = line.split(\"\\u2190\");\nassert parts.length == 2;","typeGuard":null,"tryCatchPattern":"// Wrap bootstrap setup so a parse failure surfaces a user-facing error\ntry {\n    setupBootstrap(activity);\n} catch (RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"Malformed symlink line\")) {\n        showErrorDialog(\"Bootstrap SYMLINKS.txt is corrupt: \" + e.getMessage());\n    } else throw e;\n}","preventionTips":["Only use bootstrap zips produced by the official termux-packages build.","Treat SYMLINKS.txt as machine-generated; never hand-edit delimiter lines.","Add a unit test that parses a known-good SYMLINKS.txt fixture to catch format regressions."],"tags":["bootstrap","symlink","zip-parsing","installer","termux"],"backgroundTag":null,"analyzedSha":"3df69d1da197dd9bd71a3bafd902dffd720576b4","analyzedAt":"2026-08-13T22:46:28.294Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}