date.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. 'use strict'
  2. const IMF_DAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
  3. const IMF_SPACES = [4, 7, 11, 16, 25]
  4. const IMF_MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
  5. const IMF_COLONS = [19, 22]
  6. const ASCTIME_SPACES = [3, 7, 10, 19]
  7. const RFC850_DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
  8. /**
  9. * @see https://www.rfc-editor.org/rfc/rfc9110.html#name-date-time-formats
  10. *
  11. * @param {string} date
  12. * @param {Date} [now]
  13. * @returns {Date | undefined}
  14. */
  15. function parseHttpDate (date, now) {
  16. // Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
  17. // Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
  18. // Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
  19. date = date.toLowerCase()
  20. switch (date[3]) {
  21. case ',': return parseImfDate(date)
  22. case ' ': return parseAscTimeDate(date)
  23. default: return parseRfc850Date(date, now)
  24. }
  25. }
  26. /**
  27. * @see https://httpwg.org/specs/rfc9110.html#preferred.date.format
  28. *
  29. * @param {string} date
  30. * @returns {Date | undefined}
  31. */
  32. function parseImfDate (date) {
  33. if (date.length !== 29) {
  34. return undefined
  35. }
  36. if (!date.endsWith('gmt')) {
  37. // Unsupported timezone
  38. return undefined
  39. }
  40. for (const spaceInx of IMF_SPACES) {
  41. if (date[spaceInx] !== ' ') {
  42. return undefined
  43. }
  44. }
  45. for (const colonIdx of IMF_COLONS) {
  46. if (date[colonIdx] !== ':') {
  47. return undefined
  48. }
  49. }
  50. const dayName = date.substring(0, 3)
  51. if (!IMF_DAYS.includes(dayName)) {
  52. return undefined
  53. }
  54. const dayString = date.substring(5, 7)
  55. const day = Number.parseInt(dayString)
  56. if (isNaN(day) || (day < 10 && dayString[0] !== '0')) {
  57. // Not a number, 0, or it's less than 10 and didn't start with a 0
  58. return undefined
  59. }
  60. const month = date.substring(8, 11)
  61. const monthIdx = IMF_MONTHS.indexOf(month)
  62. if (monthIdx === -1) {
  63. return undefined
  64. }
  65. const year = Number.parseInt(date.substring(12, 16))
  66. if (isNaN(year)) {
  67. return undefined
  68. }
  69. const hourString = date.substring(17, 19)
  70. const hour = Number.parseInt(hourString)
  71. if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
  72. return undefined
  73. }
  74. const minuteString = date.substring(20, 22)
  75. const minute = Number.parseInt(minuteString)
  76. if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
  77. return undefined
  78. }
  79. const secondString = date.substring(23, 25)
  80. const second = Number.parseInt(secondString)
  81. if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
  82. return undefined
  83. }
  84. return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
  85. }
  86. /**
  87. * @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
  88. *
  89. * @param {string} date
  90. * @returns {Date | undefined}
  91. */
  92. function parseAscTimeDate (date) {
  93. // This is assumed to be in UTC
  94. if (date.length !== 24) {
  95. return undefined
  96. }
  97. for (const spaceIdx of ASCTIME_SPACES) {
  98. if (date[spaceIdx] !== ' ') {
  99. return undefined
  100. }
  101. }
  102. const dayName = date.substring(0, 3)
  103. if (!IMF_DAYS.includes(dayName)) {
  104. return undefined
  105. }
  106. const month = date.substring(4, 7)
  107. const monthIdx = IMF_MONTHS.indexOf(month)
  108. if (monthIdx === -1) {
  109. return undefined
  110. }
  111. const dayString = date.substring(8, 10)
  112. const day = Number.parseInt(dayString)
  113. if (isNaN(day) || (day < 10 && dayString[0] !== ' ')) {
  114. return undefined
  115. }
  116. const hourString = date.substring(11, 13)
  117. const hour = Number.parseInt(hourString)
  118. if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
  119. return undefined
  120. }
  121. const minuteString = date.substring(14, 16)
  122. const minute = Number.parseInt(minuteString)
  123. if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
  124. return undefined
  125. }
  126. const secondString = date.substring(17, 19)
  127. const second = Number.parseInt(secondString)
  128. if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
  129. return undefined
  130. }
  131. const year = Number.parseInt(date.substring(20, 24))
  132. if (isNaN(year)) {
  133. return undefined
  134. }
  135. return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
  136. }
  137. /**
  138. * @see https://httpwg.org/specs/rfc9110.html#obsolete.date.formats
  139. *
  140. * @param {string} date
  141. * @param {Date} [now]
  142. * @returns {Date | undefined}
  143. */
  144. function parseRfc850Date (date, now = new Date()) {
  145. if (!date.endsWith('gmt')) {
  146. // Unsupported timezone
  147. return undefined
  148. }
  149. const commaIndex = date.indexOf(',')
  150. if (commaIndex === -1) {
  151. return undefined
  152. }
  153. if ((date.length - commaIndex - 1) !== 23) {
  154. return undefined
  155. }
  156. const dayName = date.substring(0, commaIndex)
  157. if (!RFC850_DAYS.includes(dayName)) {
  158. return undefined
  159. }
  160. if (
  161. date[commaIndex + 1] !== ' ' ||
  162. date[commaIndex + 4] !== '-' ||
  163. date[commaIndex + 8] !== '-' ||
  164. date[commaIndex + 11] !== ' ' ||
  165. date[commaIndex + 14] !== ':' ||
  166. date[commaIndex + 17] !== ':' ||
  167. date[commaIndex + 20] !== ' '
  168. ) {
  169. return undefined
  170. }
  171. const dayString = date.substring(commaIndex + 2, commaIndex + 4)
  172. const day = Number.parseInt(dayString)
  173. if (isNaN(day) || (day < 10 && dayString[0] !== '0')) {
  174. // Not a number, or it's less than 10 and didn't start with a 0
  175. return undefined
  176. }
  177. const month = date.substring(commaIndex + 5, commaIndex + 8)
  178. const monthIdx = IMF_MONTHS.indexOf(month)
  179. if (monthIdx === -1) {
  180. return undefined
  181. }
  182. // As of this point year is just the decade (i.e. 94)
  183. let year = Number.parseInt(date.substring(commaIndex + 9, commaIndex + 11))
  184. if (isNaN(year)) {
  185. return undefined
  186. }
  187. const currentYear = now.getUTCFullYear()
  188. const currentDecade = currentYear % 100
  189. const currentCentury = Math.floor(currentYear / 100)
  190. if (year > currentDecade && year - currentDecade >= 50) {
  191. // Over 50 years in future, go to previous century
  192. year += (currentCentury - 1) * 100
  193. } else {
  194. year += currentCentury * 100
  195. }
  196. const hourString = date.substring(commaIndex + 12, commaIndex + 14)
  197. const hour = Number.parseInt(hourString)
  198. if (isNaN(hour) || (hour < 10 && hourString[0] !== '0')) {
  199. return undefined
  200. }
  201. const minuteString = date.substring(commaIndex + 15, commaIndex + 17)
  202. const minute = Number.parseInt(minuteString)
  203. if (isNaN(minute) || (minute < 10 && minuteString[0] !== '0')) {
  204. return undefined
  205. }
  206. const secondString = date.substring(commaIndex + 18, commaIndex + 20)
  207. const second = Number.parseInt(secondString)
  208. if (isNaN(second) || (second < 10 && secondString[0] !== '0')) {
  209. return undefined
  210. }
  211. return new Date(Date.UTC(year, monthIdx, day, hour, minute, second))
  212. }
  213. module.exports = {
  214. parseHttpDate
  215. }