Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions ft8af/app/src/main/java/com/k1af/ft8af/html/LogHttpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@ static int normalizePageSize(int pageSize) {
return pageSize > 0 ? pageSize : 100;
}

/**
* The request path segment at {@code index} (from {@code getUri().split("/")}),
* or {@code null} when the request has no such segment.
*
* <p>Every {@code uriList[2]} read in {@link #serve} routes through this so a
* request that omits the trailing segment falls back to the default page instead
* of throwing {@link ArrayIndexOutOfBoundsException}. {@code GET /SHOWQSL} split to
* {@code ["", "SHOWQSL"]} (length 2), so the old bare {@code uriList[2]} threw — and
* for SHOWQSL that read sat <em>outside</em> the try/catch, so it escaped
* {@code serve} uncaught; for the DOWNQSL / DOWNQSLNOQSL {@code Content-Disposition}
* header it threw inside the try and returned an opaque HTTP 500 even though the
* body branch had already fallen back correctly. Mirrors the {@code uriList.length
* >= 3} guard the DELFOLLOW / DELQSL / DELQSLCALLSIGN branches already use.
*/
static String uriSegment(String[] uriList, int index) {
return uriList != null && index >= 0 && index < uriList.length ? uriList[index] : null;
}
Comment on lines +103 to +105

/**
* {@code Content-Disposition} filename for a per-month QSL download. Falls back to
* {@code log.adi} when the month segment is absent (see {@link #uriSegment}), rather
* than reading a missing path segment.
*/
static String downloadFileName(String month) {
return String.format("log%s.adi", month != null ? month : "");
}

@Override
public Response serve(IHTTPSession session) {
String[] uriList = session.getUri().split("/");
Expand Down Expand Up @@ -146,7 +173,8 @@ public Response serve(IHTTPSession session) {
} else if (uri.equalsIgnoreCase("SHOWALLQSL")) {
msg = HTML_STRING(showAllQSL());
} else if (uri.equalsIgnoreCase("SHOWQSL")) {
msg = HTML_STRING(showQSLByMonth(uriList[2]));
String month = uriSegment(uriList, 2);
msg = month != null ? HTML_STRING(showQSLByMonth(month)) : HtmlContext.DEFAULT_HTML();
} else if (uri.equalsIgnoreCase("DELQSLCALLSIGN")) {//Delete a QSO callsign
if (uriList.length >= 3) {
deleteQSLCallSign(uriList[2].replace("_", "/"));
Expand All @@ -164,22 +192,24 @@ public Response serve(IHTTPSession session) {
response = newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "text/plain", msg);
response.addHeader("Content-Disposition", "attachment;filename=All_log.adi");
} else if (uri.equalsIgnoreCase("DOWNQSL")) {
if (uriList.length >= 3) {
msg = downQSLByMonth(uriList[2], true);
String month = uriSegment(uriList, 2);
if (month != null) {
msg = downQSLByMonth(month, true);
} else {
msg = HtmlContext.DEFAULT_HTML();
}
response = newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "text/plain", msg);
response.addHeader("Content-Disposition", String.format("attachment;filename=log%s.adi", uriList[2]));
response.addHeader("Content-Disposition", "attachment;filename=" + downloadFileName(month));

Comment on lines 194 to 203
} else if (uri.equalsIgnoreCase("DOWNQSLNOQSL")) {
if (uriList.length >= 3) {
msg = downQSLByMonth(uriList[2], false);
String month = uriSegment(uriList, 2);
if (month != null) {
msg = downQSLByMonth(month, false);
} else {
msg = HtmlContext.DEFAULT_HTML();
}
response = newFixedLengthResponse(NanoHTTPD.Response.Status.OK, "text/plain", msg);
response.addHeader("Content-Disposition", String.format("attachment;filename=log%s.adi", uriList[2]));
response.addHeader("Content-Disposition", "attachment;filename=" + downloadFileName(month));
Comment on lines 204 to +212

} else {
response = newFixedLengthResponse(msg);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.k1af.ft8af.html;

import static com.google.common.truth.Truth.assertThat;

import org.junit.Test;

/**
* Pure-logic coverage for {@link LogHttpServer#uriSegment(String[], int)} and
* {@link LogHttpServer#downloadFileName(String)}, the guards that keep the
* web-logbook {@code serve} dispatch from reading a missing {@code uriList[2]}
* path segment.
*
* <p>Regression: {@code GET /SHOWQSL} (no trailing month) split to
* {@code ["", "SHOWQSL"]} and the bare {@code uriList[2]} read threw
* {@link ArrayIndexOutOfBoundsException}. For SHOWQSL that read was outside the
* {@code serve} try/catch, so it escaped the handler entirely; the
* DOWNQSL / DOWNQSLNOQSL {@code Content-Disposition} header read threw inside the
* try and returned an opaque HTTP 500 even though the body branch had already
* fallen back to the default page. The three guarded sibling branches
* (DELFOLLOW / DELQSL / DELQSLCALLSIGN) already used {@code uriList.length >= 3}.
*
* <p>These tests reproduce the exact {@code getUri().split("/")} shape and pin the
* guarded behaviour. No Android types are touched, so no Robolectric runner is
* needed.
*/
public class LogHttpServerUriSegmentTest {

@Test
public void missingTrailingSegment_isNull() {
// "/SHOWQSL".split("/") == ["", "SHOWQSL"] (length 2), so index 2 is absent.
String[] uriList = "/SHOWQSL".split("/");
assertThat(uriList).hasLength(2);
assertThat(LogHttpServer.uriSegment(uriList, 2)).isNull();
}

@Test
public void presentTrailingSegment_isReturned() {
String[] uriList = "/SHOWQSL/202401".split("/");
assertThat(LogHttpServer.uriSegment(uriList, 2)).isEqualTo("202401");
}

@Test
public void downloadMissingMonth_isNull() {
// The DOWNQSL Content-Disposition header used to dereference this unconditionally.
assertThat(LogHttpServer.uriSegment("/DOWNQSL".split("/"), 2)).isNull();
assertThat(LogHttpServer.uriSegment("/DOWNQSLNOQSL".split("/"), 2)).isNull();
}

@Test
public void outOfRangeAndDegenerateInputs_areNull() {
String[] one = {"", "SHOWQSL"};
assertThat(LogHttpServer.uriSegment(one, 5)).isNull();
assertThat(LogHttpServer.uriSegment(one, -1)).isNull();
assertThat(LogHttpServer.uriSegment(new String[0], 0)).isNull();
assertThat(LogHttpServer.uriSegment(null, 2)).isNull();
}
Comment on lines +49 to +56

@Test
public void downloadFileName_usesMonthWhenPresent() {
// Identical to the old String.format("log%s.adi", uriList[2]) when the month exists.
assertThat(LogHttpServer.downloadFileName("202401")).isEqualTo("log202401.adi");
assertThat(LogHttpServer.downloadFileName("")).isEqualTo("log.adi");
}

@Test
public void downloadFileName_fallsBackWhenMonthMissing() {
// A bare GET /DOWNQSL must produce a sane filename, not crash the header build.
assertThat(LogHttpServer.downloadFileName(null)).isEqualTo("log.adi");
}
}
Loading