1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
19 package org.apache.catalina.connector;
20
21 import java.io.IOException;
22
23 import org.apache.catalina.CometEvent;
24 import org.apache.catalina.Context;
25 import org.apache.catalina.Globals;
26 import org.apache.catalina.Wrapper;
27 import org.apache.catalina.util.StringManager;
28 import org.apache.catalina.util.URLEncoder;
29 import org.apache.coyote.ActionCode;
30 import org.apache.coyote.Adapter;
31 import org.apache.juli.logging.Log;
32 import org.apache.juli.logging.LogFactory;
33 import org.apache.tomcat.util.buf.B2CConverter;
34 import org.apache.tomcat.util.buf.ByteChunk;
35 import org.apache.tomcat.util.buf.CharChunk;
36 import org.apache.tomcat.util.buf.MessageBytes;
37 import org.apache.tomcat.util.http.Cookies;
38 import org.apache.tomcat.util.http.ServerCookie;
39 import org.apache.tomcat.util.net.SocketStatus;
40
41
42 /**
43 * Implementation of a request processor which delegates the processing to a
44 * Coyote processor.
45 *
46 * @author Craig R. McClanahan
47 * @author Remy Maucherat
48 * @version $Revision: 610940 $ $Date: 2008-01-10 22:40:21 +0100 (jeu., 10 janv. 2008) $
49 */
50
51 public class CoyoteAdapter
52 implements Adapter
53 {
54 private static Log log = LogFactory.getLog(CoyoteAdapter.class);
55
56 // -------------------------------------------------------------- Constants
57
58
59 public static final int ADAPTER_NOTES = 1;
60
61
62 protected static final boolean ALLOW_BACKSLASH =
63 Boolean.valueOf(System.getProperty("org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH", "false")).booleanValue();
64
65
66 // ----------------------------------------------------------- Constructors
67
68
69 /**
70 * Construct a new CoyoteProcessor associated with the specified connector.
71 *
72 * @param connector CoyoteConnector that owns this processor
73 */
74 public CoyoteAdapter(Connector connector) {
75
76 super();
77 this.connector = connector;
78
79 }
80
81
82 // ----------------------------------------------------- Instance Variables
83
84
85 /**
86 * The CoyoteConnector with which this processor is associated.
87 */
88 private Connector connector = null;
89
90
91 /**
92 * The match string for identifying a session ID parameter.
93 */
94 private static final String match =
95 ";" + Globals.SESSION_PARAMETER_NAME + "=";
96
97
98 /**
99 * The string manager for this package.
100 */
101 protected StringManager sm =
102 StringManager.getManager(Constants.Package);
103
104
105 /**
106 * Encoder for the Location URL in HTTP redirects.
107 */
108 protected static URLEncoder urlEncoder;
109
110
111 // ----------------------------------------------------- Static Initializer
112
113
114 /**
115 * The safe character set.
116 */
117 static {
118 urlEncoder = new URLEncoder();
119 urlEncoder.addSafeCharacter('-');
120 urlEncoder.addSafeCharacter('_');
121 urlEncoder.addSafeCharacter('.');
122 urlEncoder.addSafeCharacter('*');
123 urlEncoder.addSafeCharacter('/');
124 }
125
126
127 // -------------------------------------------------------- Adapter Methods
128
129
130 /**
131 * Event method.
132 *
133 * @return false to indicate an error, expected or not
134 */
135 public boolean event(org.apache.coyote.Request req,
136 org.apache.coyote.Response res, SocketStatus status) {
137
138 Request request = (Request) req.getNote(ADAPTER_NOTES);
139 Response response = (Response) res.getNote(ADAPTER_NOTES);
140
141 if (request.getWrapper() != null) {
142
143 boolean error = false;
144 boolean read = false;
145 try {
146 if (status == SocketStatus.OPEN) {
147 if (response.isClosed()) {
148 // The event has been closed asynchronously, so call end instead of
149 // read to cleanup the pipeline
150 request.getEvent().setEventType(CometEvent.EventType.END);
151 request.getEvent().setEventSubType(null);
152 } else {
153 try {
154 // Fill the read buffer of the servlet layer
155 if (request.read()) {
156 read = true;
157 }
158 } catch (IOException e) {
159 error = true;
160 }
161 if (read) {
162 request.getEvent().setEventType(CometEvent.EventType.READ);
163 request.getEvent().setEventSubType(null);
164 } else if (error) {
165 request.getEvent().setEventType(CometEvent.EventType.ERROR);
166 request.getEvent().setEventSubType(CometEvent.EventSubType.CLIENT_DISCONNECT);
167 } else {
168 request.getEvent().setEventType(CometEvent.EventType.END);
169 request.getEvent().setEventSubType(null);
170 }
171 }
172 } else if (status == SocketStatus.DISCONNECT) {
173 request.getEvent().setEventType(CometEvent.EventType.ERROR);
174 request.getEvent().setEventSubType(CometEvent.EventSubType.CLIENT_DISCONNECT);
175 error = true;
176 } else if (status == SocketStatus.ERROR) {
177 request.getEvent().setEventType(CometEvent.EventType.ERROR);
178 request.getEvent().setEventSubType(CometEvent.EventSubType.IOEXCEPTION);
179 error = true;
180 } else if (status == SocketStatus.STOP) {
181 request.getEvent().setEventType(CometEvent.EventType.END);
182 request.getEvent().setEventSubType(CometEvent.EventSubType.SERVER_SHUTDOWN);
183 } else if (status == SocketStatus.TIMEOUT) {
184 if (response.isClosed()) {
185 // The event has been closed asynchronously, so call end instead of
186 // read to cleanup the pipeline
187 request.getEvent().setEventType(CometEvent.EventType.END);
188 request.getEvent().setEventSubType(null);
189 } else {
190 request.getEvent().setEventType(CometEvent.EventType.ERROR);
191 request.getEvent().setEventSubType(CometEvent.EventSubType.TIMEOUT);
192 }
193 }
194
195 req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
196
197 // Calling the container
198 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
199
200 if (!error && !response.isClosed() && (request.getAttribute(Globals.EXCEPTION_ATTR) != null)) {
201 // An unexpected exception occurred while processing the event, so
202 // error should be called
203 request.getEvent().setEventType(CometEvent.EventType.ERROR);
204 request.getEvent().setEventSubType(null);
205 error = true;
206 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
207 }
208 if (response.isClosed() || !request.isComet()) {
209 res.action(ActionCode.ACTION_COMET_END, null);
210 } else if (!error && read && request.getAvailable()) {
211 // If this was a read and not all bytes have been read, or if no data
212 // was read from the connector, then it is an error
213 request.getEvent().setEventType(CometEvent.EventType.ERROR);
214 request.getEvent().setEventSubType(CometEvent.EventSubType.IOEXCEPTION);
215 error = true;
216 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
217 }
218 return (!error);
219 } catch (Throwable t) {
220 if (!(t instanceof IOException)) {
221 log.error(sm.getString("coyoteAdapter.service"), t);
222 }
223 error = true;
224 return false;
225 } finally {
226 req.getRequestProcessor().setWorkerThreadName(null);
227 // Recycle the wrapper request and response
228 if (error || response.isClosed() || !request.isComet()) {
229 request.recycle();
230 request.setFilterChain(null);
231 response.recycle();
232 }
233 }
234
235 } else {
236 return false;
237 }
238 }
239
240
241 /**
242 * Service method.
243 */
244 public void service(org.apache.coyote.Request req,
245 org.apache.coyote.Response res)
246 throws Exception {
247
248 Request request = (Request) req.getNote(ADAPTER_NOTES);
249 Response response = (Response) res.getNote(ADAPTER_NOTES);
250
251 if (request == null) {
252
253 // Create objects
254 request = (Request) connector.createRequest();
255 request.setCoyoteRequest(req);
256 response = (Response) connector.createResponse();
257 response.setCoyoteResponse(res);
258
259 // Link objects
260 request.setResponse(response);
261 response.setRequest(request);
262
263 // Set as notes
264 req.setNote(ADAPTER_NOTES, request);
265 res.setNote(ADAPTER_NOTES, response);
266
267 // Set query string encoding
268 req.getParameters().setQueryStringEncoding
269 (connector.getURIEncoding());
270
271 }
272
273 if (connector.getXpoweredBy()) {
274 response.addHeader("X-Powered-By", "Servlet/2.5");
275 }
276
277 boolean comet = false;
278
279 try {
280
281 // Parse and set Catalina and configuration specific
282 // request parameters
283 req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
284 if (postParseRequest(req, request, res, response)) {
285 // Calling the container
286 connector.getContainer().getPipeline().getFirst().invoke(request, response);
287
288 if (request.isComet()) {
289 if (!response.isClosed() && !response.isError()) {
290 if (request.getAvailable()) {
291 // Invoke a read event right away if there are available bytes
292 if (event(req, res, SocketStatus.OPEN)) {
293 comet = true;
294 res.action(ActionCode.ACTION_COMET_BEGIN, null);
295 }
296 } else {
297 comet = true;
298 res.action(ActionCode.ACTION_COMET_BEGIN, null);
299 }
300 } else {
301 // Clear the filter chain, as otherwise it will not be reset elsewhere
302 // since this is a Comet request
303 request.setFilterChain(null);
304 }
305 }
306
307 }
308
309 if (!comet) {
310 response.finishResponse();
311 req.action(ActionCode.ACTION_POST_REQUEST , null);
312 }
313
314 } catch (IOException e) {
315 ;
316 } catch (Throwable t) {
317 log.error(sm.getString("coyoteAdapter.service"), t);
318 } finally {
319 req.getRequestProcessor().setWorkerThreadName(null);
320 // Recycle the wrapper request and response
321 if (!comet) {
322 request.recycle();
323 response.recycle();
324 } else {
325 // Clear converters so that the minimum amount of memory
326 // is used by this processor
327 request.clearEncoders();
328 response.clearEncoders();
329 }
330 }
331
332 }
333
334
335 // ------------------------------------------------------ Protected Methods
336
337
338 /**
339 * Parse additional request parameters.
340 */
341 protected boolean postParseRequest(org.apache.coyote.Request req,
342 Request request,
343 org.apache.coyote.Response res,
344 Response response)
345 throws Exception {
346
347 // XXX the processor needs to set a correct scheme and port prior to this point,
348 // in ajp13 protocols dont make sense to get the port from the connector..
349 // XXX the processor may have set a correct scheme and port prior to this point,
350 // in ajp13 protocols dont make sense to get the port from the connector...
351 // otherwise, use connector configuration
352 if (! req.scheme().isNull()) {
353 // use processor specified scheme to determine secure state
354 request.setSecure(req.scheme().equals("https"));
355 } else {
356 // use connector scheme and secure configuration, (defaults to
357 // "http" and false respectively)
358 req.scheme().setString(connector.getScheme());
359 request.setSecure(connector.getSecure());
360 }
361
362 // FIXME: the code below doesnt belongs to here,
363 // this is only have sense
364 // in Http11, not in ajp13..
365 // At this point the Host header has been processed.
366 // Override if the proxyPort/proxyHost are set
367 String proxyName = connector.getProxyName();
368 int proxyPort = connector.getProxyPort();
369 if (proxyPort != 0) {
370 req.setServerPort(proxyPort);
371 }
372 if (proxyName != null) {
373 req.serverName().setString(proxyName);
374 }
375
376 // Parse session Id
377 parseSessionId(req, request);
378
379 // URI decoding
380 MessageBytes decodedURI = req.decodedURI();
381 decodedURI.duplicate(req.requestURI());
382
383 if (decodedURI.getType() == MessageBytes.T_BYTES) {
384 // Remove any path parameters
385 ByteChunk uriBB = decodedURI.getByteChunk();
386 int semicolon = uriBB.indexOf(';', 0);
387 if (semicolon > 0) {
388 decodedURI.setBytes
389 (uriBB.getBuffer(), uriBB.getStart(), semicolon);
390 }
391 // %xx decoding of the URL
392 try {
393 req.getURLDecoder().convert(decodedURI, false);
394 } catch (IOException ioe) {
395 res.setStatus(400);
396 res.setMessage("Invalid URI: " + ioe.getMessage());
397 return false;
398 }
399 // Normalization
400 if (!normalize(req.decodedURI())) {
401 res.setStatus(400);
402 res.setMessage("Invalid URI");
403 return false;
404 }
405 // Character decoding
406 convertURI(decodedURI, request);
407 } else {
408 // The URL is chars or String, and has been sent using an in-memory
409 // protocol handler, we have to assume the URL has been properly
410 // decoded already
411 decodedURI.toChars();
412 // Remove any path parameters
413 CharChunk uriCC = decodedURI.getCharChunk();
414 int semicolon = uriCC.indexOf(';');
415 if (semicolon > 0) {
416 decodedURI.setChars
417 (uriCC.getBuffer(), uriCC.getStart(), semicolon);
418 }
419 }
420
421 // Set the remote principal
422 String principal = req.getRemoteUser().toString();
423 if (principal != null) {
424 request.setUserPrincipal(new CoyotePrincipal(principal));
425 }
426
427 // Set the authorization type
428 String authtype = req.getAuthType().toString();
429 if (authtype != null) {
430 request.setAuthType(authtype);
431 }
432
433 // Request mapping.
434 MessageBytes serverName;
435 if (connector.getUseIPVHosts()) {
436 serverName = req.localName();
437 if (serverName.isNull()) {
438 // well, they did ask for it
439 res.action(ActionCode.ACTION_REQ_LOCAL_NAME_ATTRIBUTE, null);
440 }
441 } else {
442 serverName = req.serverName();
443 }
444 connector.getMapper().map(serverName, decodedURI,
445 request.getMappingData());
446 request.setContext((Context) request.getMappingData().context);
447 request.setWrapper((Wrapper) request.getMappingData().wrapper);
448
449 // Filter trace method
450 if (!connector.getAllowTrace()
451 && req.method().equalsIgnoreCase("TRACE")) {
452 Wrapper wrapper = request.getWrapper();
453 String header = null;
454 if (wrapper != null) {
455 String[] methods = wrapper.getServletMethods();
456 if (methods != null) {
457 for (int i=0; i<methods.length; i++) {
458 if ("TRACE".equals(methods[i])) {
459 continue;
460 }
461 if (header == null) {
462 header = methods[i];
463 } else {
464 header += ", " + methods[i];
465 }
466 }
467 }
468 }
469 res.setStatus(405);
470 res.addHeader("Allow", header);
471 res.setMessage("TRACE method is not allowed");
472 return false;
473 }
474
475 // Possible redirect
476 MessageBytes redirectPathMB = request.getMappingData().redirectPath;
477 if (!redirectPathMB.isNull()) {
478 String redirectPath = urlEncoder.encode(redirectPathMB.toString());
479 String query = request.getQueryString();
480 if (request.isRequestedSessionIdFromURL()) {
481 // This is not optimal, but as this is not very common, it
482 // shouldn't matter
483 redirectPath = redirectPath + ";" + Globals.SESSION_PARAMETER_NAME + "="
484 + request.getRequestedSessionId();
485 }
486 if (query != null) {
487 // This is not optimal, but as this is not very common, it
488 // shouldn't matter
489 redirectPath = redirectPath + "?" + query;
490 }
491 response.sendRedirect(redirectPath);
492 return false;
493 }
494
495 // Parse session Id
496 parseSessionCookiesId(req, request);
497
498 return true;
499 }
500
501
502 /**
503 * Parse session id in URL.
504 */
505 protected void parseSessionId(org.apache.coyote.Request req, Request request) {
506
507 ByteChunk uriBC = req.requestURI().getByteChunk();
508 int semicolon = uriBC.indexOf(match, 0, match.length(), 0);
509
510 if (semicolon > 0) {
511
512 // Parse session ID, and extract it from the decoded request URI
513 int start = uriBC.getStart();
514 int end = uriBC.getEnd();
515
516 int sessionIdStart = semicolon + match.length();
517 int semicolon2 = uriBC.indexOf(';', sessionIdStart);
518 if (semicolon2 >= 0) {
519 request.setRequestedSessionId
520 (new String(uriBC.getBuffer(), start + sessionIdStart,
521 semicolon2 - sessionIdStart));
522 // Extract session ID from request URI
523 byte[] buf = uriBC.getBuffer();
524 for (int i = 0; i < end - start - semicolon2; i++) {
525 buf[start + semicolon + i]
526 = buf[start + i + semicolon2];
527 }
528 uriBC.setBytes(buf, start, end - start - semicolon2 + semicolon);
529 } else {
530 request.setRequestedSessionId
531 (new String(uriBC.getBuffer(), start + sessionIdStart,
532 (end - start) - sessionIdStart));
533 uriBC.setEnd(start + semicolon);
534 }
535 request.setRequestedSessionURL(true);
536
537 } else {
538 request.setRequestedSessionId(null);
539 request.setRequestedSessionURL(false);
540 }
541
542 }
543
544
545 /**
546 * Parse session id in URL.
547 */
548 protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
549
550 // If session tracking via cookies has been disabled for the current
551 // context, don't go looking for a session ID in a cookie as a cookie
552 // from a parent context with a session ID may be present which would
553 // overwrite the valid session ID encoded in the URL
554 Context context = (Context) request.getMappingData().context;
555 if (context != null && !context.getCookies())
556 return;
557
558 // Parse session id from cookies
559 Cookies serverCookies = req.getCookies();
560 int count = serverCookies.getCookieCount();
561 if (count <= 0)
562 return;
563
564 for (int i = 0; i < count; i++) {
565 ServerCookie scookie = serverCookies.getCookie(i);
566 if (scookie.getName().equals(Globals.SESSION_COOKIE_NAME)) {
567 // Override anything requested in the URL
568 if (!request.isRequestedSessionIdFromCookie()) {
569 // Accept only the first session id cookie
570 convertMB(scookie.getValue());
571 request.setRequestedSessionId
572 (scookie.getValue().toString());
573 request.setRequestedSessionCookie(true);
574 request.setRequestedSessionURL(false);
575 if (log.isDebugEnabled())
576 log.debug(" Requested cookie session id is " +
577 request.getRequestedSessionId());
578 } else {
579 if (!request.isRequestedSessionIdValid()) {
580 // Replace the session id until one is valid
581 convertMB(scookie.getValue());
582 request.setRequestedSessionId
583 (scookie.getValue().toString());
584 }
585 }
586 }
587 }
588
589 }
590
591
592 /**
593 * Character conversion of the URI.
594 */
595 protected void convertURI(MessageBytes uri, Request request)
596 throws Exception {
597
598 ByteChunk bc = uri.getByteChunk();
599 int length = bc.getLength();
600 CharChunk cc = uri.getCharChunk();
601 cc.allocate(length, -1);
602
603 String enc = connector.getURIEncoding();
604 if (enc != null) {
605 B2CConverter conv = request.getURIConverter();
606 try {
607 if (conv == null) {
608 conv = new B2CConverter(enc);
609 request.setURIConverter(conv);
610 } else {
611 conv.recycle();
612 }
613 } catch (IOException e) {
614 // Ignore
615 log.error("Invalid URI encoding; using HTTP default");
616 connector.setURIEncoding(null);
617 }
618 if (conv != null) {
619 try {
620 conv.convert(bc, cc);
621 uri.setChars(cc.getBuffer(), cc.getStart(),
622 cc.getLength());
623 return;
624 } catch (IOException e) {
625 log.error("Invalid URI character encoding; trying ascii");
626 cc.recycle();
627 }
628 }
629 }
630
631 // Default encoding: fast conversion
632 byte[] bbuf = bc.getBuffer();
633 char[] cbuf = cc.getBuffer();
634 int start = bc.getStart();
635 for (int i = 0; i < length; i++) {
636 cbuf[i] = (char) (bbuf[i + start] & 0xff);
637 }
638 uri.setChars(cbuf, 0, length);
639
640 }
641
642
643 /**
644 * Character conversion of the a US-ASCII MessageBytes.
645 */
646 protected void convertMB(MessageBytes mb) {
647
648 // This is of course only meaningful for bytes
649 if (mb.getType() != MessageBytes.T_BYTES)
650 return;
651
652 ByteChunk bc = mb.getByteChunk();
653 CharChunk cc = mb.getCharChunk();
654 int length = bc.getLength();
655 cc.allocate(length, -1);
656
657 // Default encoding: fast conversion
658 byte[] bbuf = bc.getBuffer();
659 char[] cbuf = cc.getBuffer();
660 int start = bc.getStart();
661 for (int i = 0; i < length; i++) {
662 cbuf[i] = (char) (bbuf[i + start] & 0xff);
663 }
664 mb.setChars(cbuf, 0, length);
665
666 }
667
668
669 /**
670 * Normalize URI.
671 * <p>
672 * This method normalizes "\", "//", "/./" and "/../". This method will
673 * return false when trying to go above the root, or if the URI contains
674 * a null byte.
675 *
676 * @param uriMB URI to be normalized
677 */
678 public static boolean normalize(MessageBytes uriMB) {
679
680 ByteChunk uriBC = uriMB.getByteChunk();
681 byte[] b = uriBC.getBytes();
682 int start = uriBC.getStart();
683 int end = uriBC.getEnd();
684
685 // URL * is acceptable
686 if ((end - start == 1) && b[start] == (byte) '*')
687 return true;
688
689 int pos = 0;
690 int index = 0;
691
692 // Replace '\' with '/'
693 // Check for null byte
694 for (pos = start; pos < end; pos++) {
695 if (b[pos] == (byte) '\\') {
696 if (ALLOW_BACKSLASH) {
697 b[pos] = (byte) '/';
698 } else {
699 return false;
700 }
701 }
702 if (b[pos] == (byte) 0) {
703 return false;
704 }
705 }
706
707 // The URL must start with '/'
708 if (b[start] != (byte) '/') {
709 return false;
710 }
711
712 // Replace "//" with "/"
713 for (pos = start; pos < (end - 1); pos++) {
714 if (b[pos] == (byte) '/') {
715 while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) {
716 copyBytes(b, pos, pos + 1, end - pos - 1);
717 end--;
718 }
719 }
720 }
721
722 // If the URI ends with "/." or "/..", then we append an extra "/"
723 // Note: It is possible to extend the URI by 1 without any side effect
724 // as the next character is a non-significant WS.
725 if (((end - start) >= 2) && (b[end - 1] == (byte) '.')) {
726 if ((b[end - 2] == (byte) '/')
727 || ((b[end - 2] == (byte) '.')
728 && (b[end - 3] == (byte) '/'))) {
729 b[end] = (byte) '/';
730 end++;
731 }
732 }
733
734 uriBC.setEnd(end);
735
736 index = 0;
737
738 // Resolve occurrences of "/./" in the normalized path
739 while (true) {
740 index = uriBC.indexOf("/./", 0, 3, index);
741 if (index < 0)
742 break;
743 copyBytes(b, start + index, start + index + 2,
744 end - start - index - 2);
745 end = end - 2;
746 uriBC.setEnd(end);
747 }
748
749 index = 0;
750
751 // Resolve occurrences of "/../" in the normalized path
752 while (true) {
753 index = uriBC.indexOf("/../", 0, 4, index);
754 if (index < 0)
755 break;
756 // Prevent from going outside our context
757 if (index == 0)
758 return false;
759 int index2 = -1;
760 for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) {
761 if (b[pos] == (byte) '/') {
762 index2 = pos;
763 }
764 }
765 copyBytes(b, start + index2, start + index + 3,
766 end - start - index - 3);
767 end = end + index2 - index - 3;
768 uriBC.setEnd(end);
769 index = index2;
770 }
771
772 uriBC.setBytes(b, start, end);
773
774 return true;
775
776 }
777
778
779 // ------------------------------------------------------ Protected Methods
780
781
782 /**
783 * Copy an array of bytes to a different position. Used during
784 * normalization.
785 */
786 protected static void copyBytes(byte[] b, int dest, int src, int len) {
787 for (int pos = 0; pos < len; pos++) {
788 b[pos + dest] = b[pos + src];
789 }
790 }
791
792
793 }