From fbd5700578ccb1964e8058bffbea04f54eb2a6d0 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sun, 17 May 2026 11:34:08 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0033 (20260517T044708Z-e058) --- src/dynamic/lang/java.rs | 107 +++++- src/dynamic/lang/java_servlet_stubs.rs | 472 +++++++++++++++++++++++++ src/dynamic/lang/mod.rs | 1 + 3 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 src/dynamic/lang/java_servlet_stubs.rs diff --git a/src/dynamic/lang/java.rs b/src/dynamic/lang/java.rs index 1caf3686..e4d233cf 100644 --- a/src/dynamic/lang/java.rs +++ b/src/dynamic/lang/java.rs @@ -556,6 +556,17 @@ pub fn emit(spec: &HarnessSpec) -> Result { let shape = JavaShape::detect(spec, &entry_source); let entry_class = derive_entry_class(&entry_source); let source = generate_harness_java(spec, shape, &entry_class); + let extra_files = match shape { + // Real-world servlet sources import `javax.servlet.*` or + // `jakarta.servlet.*`; without those symbols on the classpath + // `javac` reports `package javax.servlet does not exist` and the + // verifier flips to `BuildFailed`. Stage minimal stubs alongside + // the harness so the build step links. + JavaShape::ServletDoGet | JavaShape::ServletDoPost => { + crate::dynamic::lang::java_servlet_stubs::servlet_stub_files() + } + _ => vec![], + }; Ok(HarnessSource { source, @@ -566,7 +577,7 @@ pub fn emit(spec: &HarnessSpec) -> Result { ".".to_owned(), "NyxHarness".to_owned(), ], - extra_files: vec![], + extra_files, // Stage the entry file under the public-class-derived filename // so javac's filename-vs-public-class invariant holds for both // the legacy `public class Entry` fixtures (which keep being @@ -1108,6 +1119,100 @@ mod tests { assert!(src.contains("invokeJunitTest(Vuln.class")); } + // ── Servlet stub bundle (path (a) of Phase 31 budget gate) ────────────── + + fn stage_entry(dir: &std::path::Path, name: &str, body: &str) -> String { + let path = dir.join(name); + std::fs::write(&path, body).expect("stage java entry source"); + path.to_string_lossy().into_owned() + } + + #[test] + fn emit_servlet_doget_carries_servlet_stub_bundle() { + let tmp = tempfile::TempDir::new().unwrap(); + let entry_file = stage_entry( + tmp.path(), + "Vuln.java", + "import javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doGet(HttpServletRequest r, HttpServletResponse w) {}\n}\n", + ); + let mut spec = make_spec_with(EntryKind::HttpRoute, "doGet", &entry_file); + spec.payload_slot = PayloadSlot::QueryParam("payload".into()); + let harness = emit(&spec).unwrap(); + let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + assert!( + paths.contains(&"javax/servlet/http/HttpServletRequest.java"), + "doGet bundle missing javax HttpServletRequest stub; got {paths:?}" + ); + assert!( + paths.contains(&"jakarta/servlet/http/HttpServletRequest.java"), + "doGet bundle missing jakarta HttpServletRequest stub; got {paths:?}" + ); + assert!(paths.contains(&"javax/servlet/annotation/WebServlet.java")); + assert!(paths.contains(&"javax/servlet/ServletException.java")); + } + + #[test] + fn emit_servlet_dopost_carries_servlet_stub_bundle() { + let tmp = tempfile::TempDir::new().unwrap(); + let entry_file = stage_entry( + tmp.path(), + "Vuln.java", + "import jakarta.servlet.http.HttpServletRequest;\nimport jakarta.servlet.http.HttpServletResponse;\npublic class Vuln {\n public void doPost(HttpServletRequest r, HttpServletResponse w) {}\n}\n", + ); + let mut spec = make_spec_with(EntryKind::HttpRoute, "doPost", &entry_file); + spec.payload_slot = PayloadSlot::HttpBody; + let harness = emit(&spec).unwrap(); + assert!(!harness.extra_files.is_empty(), "doPost bundle is empty"); + let paths: Vec<&str> = harness.extra_files.iter().map(|(p, _)| p.as_str()).collect(); + assert!(paths.contains(&"javax/servlet/http/HttpServlet.java")); + assert!(paths.contains(&"jakarta/servlet/http/HttpServlet.java")); + } + + #[test] + fn emit_static_method_carries_no_extra_files() { + // Regression guard: non-servlet shapes must not pay the servlet + // stub cost. Adding stubs would balloon the workdir + compile + // time for every Rust / Python / etc. harness too. + let spec = make_spec(PayloadSlot::Param(0)); + let harness = emit(&spec).unwrap(); + assert!( + harness.extra_files.is_empty(), + "non-servlet shape unexpectedly ships extra files: {:?}", + harness.extra_files.iter().map(|(p, _)| p).collect::>() + ); + } + + #[test] + fn emit_static_main_carries_no_extra_files() { + let tmp = tempfile::TempDir::new().unwrap(); + let entry_file = stage_entry( + tmp.path(), + "Vuln.java", + "public class Vuln { public static void main(String[] args) {} }\n", + ); + let spec = make_spec_with(EntryKind::CliSubcommand, "main", &entry_file); + let harness = emit(&spec).unwrap(); + assert!(harness.extra_files.is_empty()); + } + + #[test] + fn emit_spring_controller_carries_no_servlet_stubs() { + // Spring controllers do not import `javax.servlet.*`; shipping + // the bundle would still compile fine but adds dead `.class` + // files to the workdir. Keep the bundle scoped to actual + // servlet shapes. + let tmp = tempfile::TempDir::new().unwrap(); + let entry_file = stage_entry( + tmp.path(), + "Vuln.java", + "@RestController\npublic class Vuln {\n @GetMapping(\"/x\") public String run(String p) { return p; }\n}\n", + ); + let mut spec = make_spec_with(EntryKind::HttpRoute, "run", &entry_file); + spec.payload_slot = PayloadSlot::Param(0); + let harness = emit(&spec).unwrap(); + assert!(harness.extra_files.is_empty()); + } + #[test] fn entry_class_parses_public_class_declaration() { assert_eq!(derive_entry_class("public class Vuln {}"), "Vuln"); diff --git a/src/dynamic/lang/java_servlet_stubs.rs b/src/dynamic/lang/java_servlet_stubs.rs new file mode 100644 index 00000000..4880ab4e --- /dev/null +++ b/src/dynamic/lang/java_servlet_stubs.rs @@ -0,0 +1,472 @@ +//! Minimal `javax.servlet` and `jakarta.servlet` stubs for Java harnesses. +//! +//! Many real-world Java codebases (OWASP Benchmark, Spring servlet adapters, +//! legacy webapps) carry `import javax.servlet.http.HttpServletRequest;` or +//! the `jakarta.servlet` counterpart in source files the dynamic verifier +//! wants to compile. Without these symbols on the classpath, `javac` emits +//! `error: package javax.servlet does not exist` and the verifier reports +//! `BuildFailed` before any sink probe runs. +//! +//! The stubs here cover the surface the harness actually needs to link: +//! a no-arg-constructible `HttpServletRequest` whose `setParameter` / +//! `setMethod` / `setBody` setters slot into the reflective adapter in +//! `SERVLET_HELPER`, plus the small set of getters that OWASP Benchmark +//! fixtures call (`getCookies`, `getHeader`, `getInputStream`, `getReader`, +//! `getParameter`, `getParameterMap`, `getParameterNames`, +//! `getParameterValues`, `getQueryString`, `getSession`). Methods return +//! null / empty defaults; runtime correctness past compilation is the job +//! of the spec-derivation fallback paths, not the build-time stubs. +//! +//! The bundle ships both `javax.servlet` and `jakarta.servlet` so source +//! files predating the EE 9 rename and source files using the new +//! namespace both link. Each stub is generated from the same template via +//! [`make_servlet_stubs`] so the two trees stay in sync. + +/// Stub bundle for the servlet-shape Java harnesses. +/// +/// Returns `(workdir_relative_path, file_content)` pairs ready to drop into +/// [`crate::dynamic::lang::HarnessSource::extra_files`]. Subdirectories in +/// the path are created by the harness builder; the bundled files live +/// under `javax/servlet/...` and `jakarta/servlet/...` so `javac -d ` +/// drops the resulting `.class` files into matching package directories +/// and the entry source's `import javax.servlet.http.HttpServletRequest;` +/// resolves at compile time. +pub fn servlet_stub_files() -> Vec<(String, String)> { + let mut out = make_servlet_stubs("javax.servlet"); + out.extend(make_servlet_stubs("jakarta.servlet")); + out +} + +/// Render the nine-file stub set under the given package prefix +/// (`javax.servlet` or `jakarta.servlet`). +fn make_servlet_stubs(pkg: &str) -> Vec<(String, String)> { + let pkg_path = pkg.replace('.', "/"); + let http = format!("{pkg}.http"); + let http_path = format!("{pkg_path}/http"); + vec![ + ( + format!("{pkg_path}/ServletException.java"), + servlet_exception(pkg), + ), + ( + format!("{pkg_path}/ServletInputStream.java"), + servlet_input_stream(pkg), + ), + ( + format!("{pkg_path}/RequestDispatcher.java"), + request_dispatcher(pkg, &http), + ), + ( + format!("{pkg_path}/annotation/WebServlet.java"), + web_servlet(pkg), + ), + (format!("{http_path}/HttpServlet.java"), http_servlet(pkg)), + ( + format!("{http_path}/HttpServletRequest.java"), + http_servlet_request(pkg, &http), + ), + ( + format!("{http_path}/HttpServletResponse.java"), + http_servlet_response(&http), + ), + ( + format!("{http_path}/HttpSession.java"), + http_session(&http), + ), + (format!("{http_path}/Cookie.java"), cookie(&http)), + ] +} + +fn servlet_exception(pkg: &str) -> String { + format!( + r#"package {pkg}; +public class ServletException extends Exception {{ + public ServletException() {{ super(); }} + public ServletException(String msg) {{ super(msg); }} + public ServletException(String msg, Throwable cause) {{ super(msg, cause); }} + public ServletException(Throwable cause) {{ super(cause); }} +}} +"# + ) +} + +fn servlet_input_stream(pkg: &str) -> String { + format!( + r#"package {pkg}; +import java.io.InputStream; +import java.io.IOException; +public abstract class ServletInputStream extends InputStream {{ + protected ServletInputStream() {{}} + @Override public int read() throws IOException {{ return -1; }} +}} +"# + ) +} + +fn request_dispatcher(pkg: &str, http: &str) -> String { + format!( + r#"package {pkg}; +import {http}.HttpServletRequest; +import {http}.HttpServletResponse; +import java.io.IOException; +public interface RequestDispatcher {{ + void forward(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; + void include(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException; +}} +"# + ) +} + +fn web_servlet(pkg: &str) -> String { + format!( + r#"package {pkg}.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WebServlet {{ + String[] value() default {{}}; + String[] urlPatterns() default {{}}; + String name() default ""; + int loadOnStartup() default -1; + boolean asyncSupported() default false; +}} +"# + ) +} + +fn http_servlet(pkg: &str) -> String { + format!( + r#"package {pkg}.http; +import {pkg}.ServletException; +import java.io.IOException; +public abstract class HttpServlet {{ + public HttpServlet() {{}} + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {{}} + public String getServletInfo() {{ return ""; }} + public void init() throws ServletException {{}} + public void destroy() {{}} +}} +"# + ) +} + +fn http_servlet_request(pkg: &str, http: &str) -> String { + format!( + r#"package {http}; +import {pkg}.RequestDispatcher; +import {pkg}.ServletInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +public class HttpServletRequest {{ + private final Map params = new HashMap<>(); + private String method = "GET"; + private String body = ""; + public HttpServletRequest() {{}} + public void setParameter(String name, String value) {{ params.put(name, value); }} + public void setMethod(String m) {{ this.method = m; }} + public void setBody(String b) {{ this.body = b == null ? "" : b; }} + public String getBody() {{ return body; }} + public String getParameter(String name) {{ return params.get(name); }} + public String[] getParameterValues(String name) {{ + String v = params.get(name); + return v == null ? null : new String[] {{ v }}; + }} + public Map getParameterMap() {{ + Map m = new HashMap<>(); + for (Map.Entry e : params.entrySet()) {{ + m.put(e.getKey(), new String[] {{ e.getValue() }}); + }} + return m; + }} + public Enumeration getParameterNames() {{ return Collections.enumeration(params.keySet()); }} + public String getHeader(String name) {{ return null; }} + public Enumeration getHeaders(String name) {{ return Collections.emptyEnumeration(); }} + public Enumeration getHeaderNames() {{ return Collections.emptyEnumeration(); }} + public int getIntHeader(String name) {{ return -1; }} + public long getDateHeader(String name) {{ return -1L; }} + public Cookie[] getCookies() {{ return null; }} + public HttpSession getSession() {{ return new HttpSession(); }} + public HttpSession getSession(boolean create) {{ return new HttpSession(); }} + public ServletInputStream getInputStream() throws IOException {{ return null; }} + public BufferedReader getReader() throws IOException {{ return new BufferedReader(new StringReader(body)); }} + public String getMethod() {{ return method; }} + public String getQueryString() {{ return null; }} + public StringBuffer getRequestURL() {{ return new StringBuffer(); }} + public String getRequestURI() {{ return ""; }} + public String getRemoteAddr() {{ return "127.0.0.1"; }} + public String getRemoteHost() {{ return "localhost"; }} + public String getServletPath() {{ return ""; }} + public String getContextPath() {{ return ""; }} + public String getPathInfo() {{ return null; }} + public String getPathTranslated() {{ return null; }} + public Object getAttribute(String name) {{ return null; }} + public void setAttribute(String name, Object value) {{}} + public void removeAttribute(String name) {{}} + public Enumeration getAttributeNames() {{ return Collections.emptyEnumeration(); }} + public String getCharacterEncoding() {{ return "UTF-8"; }} + public void setCharacterEncoding(String enc) {{}} + public String getContentType() {{ return null; }} + public int getContentLength() {{ return body == null ? 0 : body.length(); }} + public long getContentLengthLong() {{ return getContentLength(); }} + public String getProtocol() {{ return "HTTP/1.1"; }} + public String getScheme() {{ return "http"; }} + public String getServerName() {{ return "localhost"; }} + public int getServerPort() {{ return 80; }} + public Locale getLocale() {{ return Locale.getDefault(); }} + public Enumeration getLocales() {{ return Collections.enumeration(Collections.singletonList(Locale.getDefault())); }} + public RequestDispatcher getRequestDispatcher(String path) {{ return null; }} + public boolean isSecure() {{ return false; }} + public String getAuthType() {{ return null; }} + public String getRemoteUser() {{ return null; }} + public java.security.Principal getUserPrincipal() {{ return null; }} + public boolean isUserInRole(String role) {{ return false; }} + public String getRequestedSessionId() {{ return null; }} + public boolean isRequestedSessionIdValid() {{ return false; }} + public boolean isRequestedSessionIdFromCookie() {{ return false; }} + public boolean isRequestedSessionIdFromURL() {{ return false; }} +}} +"# + ) +} + +fn http_servlet_response(http: &str) -> String { + format!( + r#"package {http}; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +public class HttpServletResponse {{ + public static final int SC_OK = 200; + public static final int SC_NOT_FOUND = 404; + public static final int SC_FORBIDDEN = 403; + public static final int SC_UNAUTHORIZED = 401; + public static final int SC_INTERNAL_SERVER_ERROR = 500; + public static final int SC_MOVED_PERMANENTLY = 301; + public static final int SC_MOVED_TEMPORARILY = 302; + private final StringWriter sw = new StringWriter(); + private final PrintWriter pw = new PrintWriter(sw); + private int status = SC_OK; + public HttpServletResponse() {{}} + public PrintWriter getWriter() throws IOException {{ return pw; }} + public String getBody() {{ pw.flush(); return sw.toString(); }} + public OutputStream getOutputStream() throws IOException {{ return new java.io.ByteArrayOutputStream(); }} + public void setContentType(String type) {{}} + public String getContentType() {{ return null; }} + public void setCharacterEncoding(String enc) {{}} + public String getCharacterEncoding() {{ return "UTF-8"; }} + public void setContentLength(int len) {{}} + public void setContentLengthLong(long len) {{}} + public void setStatus(int sc) {{ this.status = sc; }} + public int getStatus() {{ return status; }} + public void sendError(int sc) throws IOException {{ this.status = sc; }} + public void sendError(int sc, String msg) throws IOException {{ this.status = sc; }} + public void sendRedirect(String location) throws IOException {{ this.status = SC_MOVED_TEMPORARILY; }} + public void addCookie(Cookie cookie) {{}} + public void setHeader(String name, String value) {{}} + public void addHeader(String name, String value) {{}} + public void setIntHeader(String name, int value) {{}} + public void addIntHeader(String name, int value) {{}} + public void setDateHeader(String name, long date) {{}} + public void addDateHeader(String name, long date) {{}} + public boolean containsHeader(String name) {{ return false; }} + public String getHeader(String name) {{ return null; }} + public java.util.Collection getHeaders(String name) {{ return java.util.Collections.emptyList(); }} + public java.util.Collection getHeaderNames() {{ return java.util.Collections.emptyList(); }} + public String encodeURL(String url) {{ return url; }} + public String encodeRedirectURL(String url) {{ return url; }} + public void flushBuffer() throws IOException {{}} + public void resetBuffer() {{}} + public void reset() {{}} + public boolean isCommitted() {{ return false; }} + public void setBufferSize(int size) {{}} + public int getBufferSize() {{ return 0; }} + public void setLocale(java.util.Locale loc) {{}} + public java.util.Locale getLocale() {{ return java.util.Locale.getDefault(); }} +}} +"# + ) +} + +fn http_session(http: &str) -> String { + format!( + r#"package {http}; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +public class HttpSession {{ + private final Map attrs = new HashMap<>(); + public HttpSession() {{}} + public String getId() {{ return "stub-session"; }} + public void setAttribute(String name, Object value) {{ attrs.put(name, value); }} + public Object getAttribute(String name) {{ return attrs.get(name); }} + public void removeAttribute(String name) {{ attrs.remove(name); }} + public Enumeration getAttributeNames() {{ return Collections.enumeration(attrs.keySet()); }} + public long getCreationTime() {{ return 0L; }} + public long getLastAccessedTime() {{ return 0L; }} + public int getMaxInactiveInterval() {{ return 0; }} + public void setMaxInactiveInterval(int interval) {{}} + public boolean isNew() {{ return true; }} + public void invalidate() {{}} + public void putValue(String name, Object value) {{ attrs.put(name, value); }} + public Object getValue(String name) {{ return attrs.get(name); }} + public String[] getValueNames() {{ return attrs.keySet().toArray(new String[0]); }} + public void removeValue(String name) {{ attrs.remove(name); }} +}} +"# + ) +} + +fn cookie(http: &str) -> String { + format!( + r#"package {http}; +public class Cookie implements Cloneable {{ + private String name; + private String value; + private String path; + private String domain; + private String comment; + private int maxAge = -1; + private int version = 0; + private boolean secure; + private boolean httpOnly; + public Cookie() {{}} + public Cookie(String name, String value) {{ this.name = name; this.value = value; }} + public String getName() {{ return name; }} + public String getValue() {{ return value; }} + public void setValue(String value) {{ this.value = value; }} + public void setPath(String path) {{ this.path = path; }} + public String getPath() {{ return path; }} + public void setDomain(String domain) {{ this.domain = domain; }} + public String getDomain() {{ return domain; }} + public void setMaxAge(int age) {{ this.maxAge = age; }} + public int getMaxAge() {{ return maxAge; }} + public void setSecure(boolean secure) {{ this.secure = secure; }} + public boolean getSecure() {{ return secure; }} + public void setHttpOnly(boolean httpOnly) {{ this.httpOnly = httpOnly; }} + public boolean isHttpOnly() {{ return httpOnly; }} + public void setVersion(int v) {{ this.version = v; }} + public int getVersion() {{ return version; }} + public void setComment(String c) {{ this.comment = c; }} + public String getComment() {{ return comment; }} + @Override public Object clone() {{ try {{ return super.clone(); }} catch (CloneNotSupportedException e) {{ throw new RuntimeException(e); }} }} +}} +"# + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ships_both_javax_and_jakarta_trees() { + let files = servlet_stub_files(); + let paths: Vec<&str> = files.iter().map(|(p, _)| p.as_str()).collect(); + assert!(paths.contains(&"javax/servlet/http/HttpServletRequest.java")); + assert!(paths.contains(&"javax/servlet/ServletException.java")); + assert!(paths.contains(&"javax/servlet/annotation/WebServlet.java")); + assert!(paths.contains(&"jakarta/servlet/http/HttpServletRequest.java")); + assert!(paths.contains(&"jakarta/servlet/ServletException.java")); + assert!(paths.contains(&"jakarta/servlet/annotation/WebServlet.java")); + } + + #[test] + fn bundle_has_eighteen_files() { + // Nine stubs per package tree, two trees. A drift here usually + // means a stub was added without updating the count assertion or + // a stub got accidentally dropped. + let files = servlet_stub_files(); + assert_eq!(files.len(), 18, "expected 9 javax + 9 jakarta stubs"); + } + + #[test] + fn http_servlet_request_carries_owasp_method_surface() { + // OWASP Benchmark v1.2 fixtures exercise this surface; if any of + // these getters disappears the bundle will start producing + // BuildFailed verdicts on the corresponding fixtures. + let req = http_servlet_request("javax.servlet", "javax.servlet.http"); + for method in &[ + "getCookies", + "getHeader", + "getHeaderNames", + "getHeaders", + "getInputStream", + "getParameter", + "getParameterMap", + "getParameterNames", + "getParameterValues", + "getQueryString", + "getSession", + "getReader", + ] { + assert!( + req.contains(method), + "javax HttpServletRequest stub missing `{method}`" + ); + } + } + + #[test] + fn http_servlet_response_carries_owasp_method_surface() { + let resp = http_servlet_response("javax.servlet.http"); + for method in &["addCookie", "getWriter", "setContentType"] { + assert!( + resp.contains(method), + "javax HttpServletResponse stub missing `{method}`" + ); + } + } + + #[test] + fn http_servlet_request_keeps_reflective_hook_setters() { + // The Java emitter's SERVLET_HELPER uses reflection to invoke + // setParameter / setMethod / setBody on the stub request before + // the entry method runs. Dropping any of these would silently + // break payload seeding for OWASP-shape harnesses. + for pkg in &["javax.servlet", "jakarta.servlet"] { + let http = format!("{pkg}.http"); + let req = http_servlet_request(pkg, &http); + for method in &["setParameter", "setMethod", "setBody"] { + assert!( + req.contains(method), + "{pkg} HttpServletRequest stub missing `{method}`" + ); + } + } + } + + #[test] + fn jakarta_stubs_carry_jakarta_package_declaration() { + let files = servlet_stub_files(); + for (path, body) in &files { + if path.starts_with("jakarta/") { + assert!( + body.contains("package jakarta.servlet"), + "jakarta stub at {path} missing jakarta package declaration" + ); + assert!( + !body.contains("package javax.servlet"), + "jakarta stub at {path} accidentally carries javax package" + ); + } + } + } +} diff --git a/src/dynamic/lang/mod.rs b/src/dynamic/lang/mod.rs index 91df721f..fc1b7a77 100644 --- a/src/dynamic/lang/mod.rs +++ b/src/dynamic/lang/mod.rs @@ -16,6 +16,7 @@ pub mod c; pub mod cpp; pub mod go; pub mod java; +pub mod java_servlet_stubs; pub mod javascript; pub mod js_shared; pub mod php;