diff --git a/bantam.hpp b/bantam.hpp index 269e6f9..c03cf06 100644 --- a/bantam.hpp +++ b/bantam.hpp @@ -197,8 +197,8 @@ int rune_width(int cp); void mkdir_p(std::string_view dir); std::string project_id(); -std::string bantam_tmp_dir(); -std::string tool_res_dir(); +std::string bantam_tmp_dir(std::string_view sess_id = ""); +std::string tool_res_dir(std::string_view sess_id = ""); std::string md5_hex(std::string_view data); /* ------------------------------------------------------------------ */ diff --git a/main.cpp b/main.cpp index 0337f09..d7527db 100644 --- a/main.cpp +++ b/main.cpp @@ -229,6 +229,8 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s else if (!has_line && !has_byte && !has_del_lines && !has_del_bytes) { /* No offset/deletion parameters: overwrite the whole file. */ std::string c = filter_text(cv); + /* Ensure the written file ends with a single trailing newline. */ + if (!c.empty() && c.back() != '\n') c.push_back('\n'); auto sl = fp.find_last_of('/'); if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl)); else mkdir_p("."); @@ -236,6 +238,10 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s else { res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); } } else { int werr = 0; std::string c = filter_text(cv); + /* Line-based edits keep the text-file trailing-newline convention; + * byte-based edits are exact byte operations and must not have a + * newline injected. */ + if (!(has_byte || has_del_bytes) && !c.empty() && c.back() != '\n') c.push_back('\n'); res = write_file(fp, line_off, byte_off, del_lines, del_bytes, c, &werr); if (werr) sty = 31; } diff --git a/tools.cpp b/tools.cpp index ea5c05c..ffcf10f 100644 --- a/tools.cpp +++ b/tools.cpp @@ -291,6 +291,13 @@ std::string write_file(std::string_view path, long line_offset, long byte_offset nd.append(data, 0, prefix_len); nd.append(content); if (suffix_len) nd.append(data, end_del, suffix_len); + /* Keep a single trailing newline for line-based / whole-file text writes. + * Byte-based edits are exact byte operations and must not be mutated with an + * injected newline, so the guard is skipped whenever a byte offset or byte + * deletion is actually in effect. */ + bool byte_based = (line_offset < 0 && byte_offset >= 0) || + (del_lines < 0 && del_bytes >= 0); + if (!byte_based && !nd.empty() && nd.back() != '\n') nd.push_back('\n'); if (!write_file_str(p, nd)) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); diff --git a/util.cpp b/util.cpp index c7bf5e8..da49fa1 100644 --- a/util.cpp +++ b/util.cpp @@ -192,15 +192,27 @@ void mkdir_p(std::string_view dir) { } mkdir(path.c_str(), 0755); } -std::string bantam_tmp_dir() { +std::string bantam_tmp_dir(std::string_view sess_id) { const char *tmp = std::getenv("TMPDIR"); if (!tmp || !*tmp) tmp = "/tmp"; - std::string p = std::string(tmp) + "/bantam"; + std::string base(tmp); + /* Drop a single trailing slash so we never build a "//bantam" path (which + * some platforms treat as a special "super-root"); keep "/" itself intact. */ + if (base.size() > 1 && base.back() == '/') base.pop_back(); + std::string p = base; + if (p != "/") p += "/"; + p += "bantam"; + /* Segregate temporary artifacts per session so different projects/sessions + * never share or collide on scratch files. */ + std::string sid(sess_id); + if (sid.empty()) sid = project_id(); + if (!sid.empty()) { p += "/"; p += sid; } mkdir_p(p); return p; } -std::string tool_res_dir() { - std::string base = bantam_tmp_dir(); + +std::string tool_res_dir(std::string_view sess_id) { + std::string base = bantam_tmp_dir(sess_id); std::string p = base + "/toolres"; mkdir_p(p); return p;